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

Add PlusMinus icon and allow negative values in MoneyRequestAmountInput #56092

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2e03057
Add PlusMinus icon and allow negative values in MoneyRequestAmountInput
pasyukevich Jan 30, 2025
0fe0e70
Merge branch 'main' into feature/allow-negative
pasyukevich Jan 30, 2025
7f331c8
Refactor onFlipAmount to handle amount parsing and update selection i…
pasyukevich Jan 30, 2025
0c214e4
Remove allowBubble prop from buttons in MoneyRequestAmountForm for cl…
pasyukevich Jan 30, 2025
b22f136
Prevent flipping amount when current amount is zero in MoneyRequestAm…
pasyukevich Jan 30, 2025
83530b9
onFlipAmount update for undefined value
pasyukevich Jan 30, 2025
396aed7
Update onFlipAmount to handle undefined and zero values in MoneyReque…
pasyukevich Jan 30, 2025
150ea82
Refactor amount parsing in MoneyRequestAmountForm to use parseFloat a…
pasyukevich Jan 30, 2025
edc0902
Update getTransactionAmount call to allow negative
pasyukevich Jan 30, 2025
95d0472
Add support for negative transaction amounts in getTransactionDetails…
pasyukevich Jan 31, 2025
60025b5
Update getTransactionDetails call to include additional parameters fo…
pasyukevich Jan 31, 2025
661602b
Add translation for 'Flip' in English and Spanish, and update button …
pasyukevich Jan 31, 2025
588d50c
Merge branch 'main' into feature/allow-negative
pasyukevich Feb 6, 2025
278761a
Add support for negative amounts in BaseTextInputWithCurrencySymbol
pasyukevich Feb 6, 2025
c0ce336
Refactor MoneyRequestAmountInput to use isNegative prop instead of al…
pasyukevich Feb 7, 2025
3e48c80
Enhance MoneyRequestAmountInput and MoneyRequestAmountForm to support…
pasyukevich Feb 7, 2025
fc17d4e
Revert onCurrencyButtonPress optional
pasyukevich Feb 7, 2025
bf9a26f
Refactor MoneyRequestAmountInput and BaseTextInputWithCurrencySymbol …
pasyukevich Feb 7, 2025
81c19c9
Remove optional type declaration for allowNegativeAmount parameter in…
pasyukevich Feb 7, 2025
b698d3b
Enable currency flipping in IOURequestStepAmount component
pasyukevich Feb 7, 2025
52b1899
Merge branch 'main' into feature/allow-negative
pasyukevich Feb 7, 2025
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
7 changes: 7 additions & 0 deletions assets/images/plus-minus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,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';
Expand Down Expand Up @@ -427,5 +428,6 @@ export {
GalleryNotFound,
Train,
boltSlash,
PlusMinus,
MagnifyingGlassSpyMouthClosed,
};
33 changes: 18 additions & 15 deletions src/components/MoneyRequestAmountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -92,6 +92,9 @@ type MoneyRequestAmountInputProps = {

/** The width of inner content */
contentWidth?: number;

/** Whether the amount is negative */
isNegative?: boolean;
} & Pick<TextInputWithCurrencySymbolProps, 'autoGrowExtraSpace'>;

type Selection = {
Expand All @@ -107,7 +110,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(
{
Expand All @@ -129,6 +132,7 @@ function MoneyRequestAmountInput(
autoGrow = true,
autoGrowExtraSpace,
contentWidth,
isNegative = false,
...props
}: MoneyRequestAmountInputProps,
forwardedRef: ForwardedRef<BaseTextInputRef>,
Expand All @@ -139,7 +143,7 @@ function MoneyRequestAmountInput(

const amountRef = useRef<string | undefined>(undefined);

const decimals = CurrencyUtils.getCurrencyDecimals(currency);
const decimals = getCurrencyDecimals(currency);
const selectedAmountAsString = amount ? onFormatAmount(amount, currency) : '';

const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
Expand All @@ -161,13 +165,11 @@ function MoneyRequestAmountInput(
(newAmount: string) => {
// 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;
}
Expand All @@ -176,7 +178,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;
Expand Down Expand Up @@ -233,12 +235,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
Expand All @@ -249,7 +251,7 @@ function MoneyRequestAmountInput(
*/
const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent<KeyboardEvent>) => {
const key = nativeEvent?.key.toLowerCase();
if (Browser.isMobileSafari() && key === CONST.PLATFORM_SPECIFIC_KEYS.CTRL.DEFAULT) {
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;
Expand All @@ -276,7 +278,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<Element, MouseEvent>) => {
Expand Down Expand Up @@ -340,6 +342,7 @@ function MoneyRequestAmountInput(
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
contentWidth={contentWidth}
isNegative={isNegative}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -22,14 +23,15 @@ function BaseTextInputWithCurrencySymbol(
isCurrencyPressable = true,
hideCurrencySymbol = false,
extraSymbol,
isNegative = false,
style,
...rest
}: BaseTextInputWithCurrencySymbolProps,
ref: React.ForwardedRef<BaseTextInputRef>,
) {
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 && (
Expand All @@ -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);
};

Expand All @@ -67,9 +69,12 @@ function BaseTextInputWithCurrencySymbol(
/>
);

if (isCurrencySymbolLTR) {
const negativeSymbol = <Text style={[styles.iouAmountText]}>-</Text>;

if (shouldShowCurrencySymbolLTR) {
return (
<>
{isNegative && negativeSymbol}
{currencySymbolButton}
{amountTextInput}
{extraSymbol}
Expand All @@ -79,6 +84,7 @@ function BaseTextInputWithCurrencySymbol(

return (
<>
{isNegative && negativeSymbol}
{amountTextInput}
{currencySymbolButton}
{extraSymbol}
Expand Down
3 changes: 3 additions & 0 deletions src/components/TextInputWithCurrencySymbol/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ type BaseTextInputWithCurrencySymbolProps = {

/** Hide the focus styles on TextInput */
hideFocusedState?: boolean;

/** Whether the amount is negative */
isNegative?: boolean;
} & Pick<BaseTextInputProps, 'autoFocus' | 'autoGrow' | 'autoGrowExtraSpace' | 'contentWidth' | 'onPress'>;

type TextInputWithCurrencySymbolProps = Omit<BaseTextInputWithCurrencySymbolProps, 'onSelectionChange'> & {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,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}` : ''}`,
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,7 @@ const translations = {
payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`),
nextStep: 'Pasos siguientes',
finished: 'Finalizado',
flip: 'Voltear',
sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`,
submitAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
submittedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicitó ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
Expand Down
9 changes: 7 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3438,14 +3438,19 @@ function getMoneyRequestReportName({
* into a flat object. Used for displaying transactions and sending them in API commands
*/

function getTransactionDetails(transaction: OnyxInputOrEntry<Transaction>, createdDateFormat: string = CONST.DATE.FNS_FORMAT_STRING): TransactionDetails | undefined {
function getTransactionDetails(
transaction: OnyxInputOrEntry<Transaction>,
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),
Expand Down
17 changes: 13 additions & 4 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,9 @@ function getDescription(transaction: OnyxInputOrEntry<Transaction>): string {
/**
* Return the amount field from the transaction, return the modifiedAmount if present.
*/
function getAmount(transaction: OnyxInputOrEntry<Transaction>, isFromExpenseReport = false, isFromTrackedExpense = false): number {
function getAmount(transaction: OnyxInputOrEntry<Transaction>, 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);
Expand All @@ -505,12 +505,21 @@ function getAmount(transaction: OnyxInputOrEntry<Transaction>, 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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3317,7 +3317,7 @@ function getUpdateMoneyRequestParams(
policy,
})
: 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
Expand Down
Loading
Loading