Skip to content

Commit 894c310

Browse files
committed
Preserve transactions amount in create IOU
1 parent 8ae5491 commit 894c310

File tree

8 files changed

+69
-22
lines changed

8 files changed

+69
-22
lines changed

src/components/transactionPropTypes.js

+3
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,7 @@ export default PropTypes.shape({
9393

9494
/** Server side errors keyed by microtime */
9595
errorFields: PropTypes.objectOf(PropTypes.objectOf(translatableTextPropTypes)),
96+
97+
/** Whether the original input should be shown */
98+
shouldShowOriginalAmount: PropTypes.bool,
9699
});

src/libs/CurrencyUtils.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,22 @@ function convertToBackendAmount(amountAsFloat: number): number {
8787
*
8888
* @note we do not support any currencies with more than two decimal places.
8989
*/
90-
function convertToFrontendAmount(amountAsInt: number): number {
90+
function convertToFrontendAmountAsInteger(amountAsInt: number): number {
9191
return Math.trunc(amountAsInt) / 100.0;
9292
}
93+
94+
/**
95+
* Takes an amount in "cents" as an integer and converts it to a string amount used in the frontend.
96+
*
97+
* @note we do not support any currencies with more than two decimal places.
98+
*/
99+
function convertToFrontendAmountAsString(amountAsInt: number | null | undefined): string {
100+
if (amountAsInt === null || amountAsInt === undefined) {
101+
return '';
102+
}
103+
return convertToFrontendAmountAsInteger(amountAsInt).toFixed(2);
104+
}
105+
93106
/**
94107
* Given an amount in the "cents", convert it to a string for display in the UI.
95108
* The backend always handle things in "cents" (subunit equal to 1/100)
@@ -98,7 +111,7 @@ function convertToFrontendAmount(amountAsInt: number): number {
98111
* @param currency - IOU currency
99112
*/
100113
function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string {
101-
const convertedAmount = convertToFrontendAmount(amountInCents);
114+
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents);
102115
return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
103116
style: 'currency',
104117
currency,
@@ -139,7 +152,8 @@ export {
139152
getCurrencySymbol,
140153
isCurrencySymbolLTR,
141154
convertToBackendAmount,
142-
convertToFrontendAmount,
155+
convertToFrontendAmountAsInteger,
156+
convertToFrontendAmountAsString,
143157
convertToDisplayString,
144158
convertAmountToDisplayString,
145159
isValidCurrencyCode,

src/libs/actions/IOU.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,12 @@ function startMoneyRequest(iouType: ValueOf<typeof CONST.IOU.TYPE>, reportID: st
357357
}
358358

359359
// eslint-disable-next-line @typescript-eslint/naming-convention
360-
function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string, removeOriginalCurrency = false) {
360+
function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string, removeOriginalCurrency = false, shouldShowOriginalAmount = false) {
361361
if (removeOriginalCurrency) {
362-
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency, originalCurrency: null});
362+
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency, originalCurrency: null, shouldShowOriginalAmount});
363363
return;
364364
}
365-
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency});
365+
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency, shouldShowOriginalAmount});
366366
}
367367

368368
// eslint-disable-next-line @typescript-eslint/naming-convention

src/pages/iou/request/IOURequestStartPage.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,14 @@ function IOURequestStartPage({
166166
onTabSelected={resetIOUTypeIfChanged}
167167
tabBar={TabSelector}
168168
>
169-
<TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>{() => <IOURequestStepAmount route={route} />}</TopTab.Screen>
169+
<TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>
170+
{() => (
171+
<IOURequestStepAmount
172+
shouldKeepUserInput
173+
route={route}
174+
/>
175+
)}
176+
</TopTab.Screen>
170177
<TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen>
171178
{shouldDisplayDistanceRequest && <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>{() => <IOURequestStepDistance route={route} />}</TopTab.Screen>}
172179
</OnyxTabNavigator>

src/pages/iou/request/step/IOURequestStepAmount.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useFocusEffect} from '@react-navigation/native';
22
import lodashGet from 'lodash/get';
33
import lodashIsEmpty from 'lodash/isEmpty';
4+
import PropTypes from 'prop-types';
45
import React, {useCallback, useEffect, useRef} from 'react';
56
import {withOnyx} from 'react-native-onyx';
67
import transactionPropTypes from '@components/transactionPropTypes';
@@ -39,13 +40,17 @@ const propTypes = {
3940

4041
/** The draft transaction object being modified in Onyx */
4142
draftTransaction: transactionPropTypes,
43+
44+
/** Whether the user input should be kept or not */
45+
shouldKeepUserInput: PropTypes.bool,
4246
};
4347

4448
const defaultProps = {
4549
report: {},
4650
transaction: {},
4751
splitDraftTransaction: {},
4852
draftTransaction: {},
53+
shouldKeepUserInput: false,
4954
};
5055

5156
function IOURequestStepAmount({
@@ -56,6 +61,7 @@ function IOURequestStepAmount({
5661
transaction,
5762
splitDraftTransaction,
5863
draftTransaction,
64+
shouldKeepUserInput,
5965
}) {
6066
const {translate} = useLocalize();
6167
const textInput = useRef(null);
@@ -125,7 +131,7 @@ function IOURequestStepAmount({
125131
isSaveButtonPressed.current = true;
126132
const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(amount));
127133

128-
IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, true);
134+
IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD, true, shouldKeepUserInput);
129135

130136
if (backTo) {
131137
Navigation.goBack(backTo);
@@ -183,6 +189,7 @@ function IOURequestStepAmount({
183189
currency={currency}
184190
amount={Math.abs(transactionAmount)}
185191
ref={(e) => (textInput.current = e)}
192+
shouldKeepUserInput={transaction.shouldShowOriginalAmount}
186193
onCurrencyButtonPress={navigateToCurrencySelectionPage}
187194
onSubmitButtonPress={saveAmountAndCurrency}
188195
selectedTab={iouRequestType}

src/pages/iou/steps/MoneyRequestAmountForm.tsx

+10-11
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type MoneyRequestAmountFormProps = {
4949

5050
/** The current tab we have navigated to in the request modal. String that corresponds to the request type. */
5151
selectedTab?: SelectedTabRequest;
52+
53+
/** Whether the user input should be kept or not */
54+
shouldKeepUserInput?: boolean;
5255
};
5356

5457
type Selection = {
@@ -66,7 +69,7 @@ const getNewSelection = (oldSelection: Selection, prevLength: number, newLength:
6669

6770
const isAmountInvalid = (amount: string) => !amount.length || parseFloat(amount) < 0.01;
6871
const isTaxAmountInvalid = (currentAmount: string, taxAmount: number, isTaxAmountForm: boolean) =>
69-
isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmount(Math.abs(taxAmount));
72+
isTaxAmountForm && Number.parseFloat(currentAmount) > CurrencyUtils.convertToFrontendAmountAsInteger(Math.abs(taxAmount));
7073

7174
const AMOUNT_VIEW_ID = 'amountView';
7275
const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView';
@@ -82,6 +85,7 @@ function MoneyRequestAmountForm(
8285
onCurrencyButtonPress,
8386
onSubmitButtonPress,
8487
selectedTab = CONST.TAB_REQUEST.MANUAL,
88+
shouldKeepUserInput = false,
8589
}: MoneyRequestAmountFormProps,
8690
forwardedRef: ForwardedRef<BaseTextInputRef>,
8791
) {
@@ -93,7 +97,7 @@ function MoneyRequestAmountForm(
9397
const isTaxAmountForm = Navigation.getActiveRoute().includes('taxAmount');
9498

9599
const decimals = CurrencyUtils.getCurrencyDecimals(currency);
96-
const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : '';
100+
const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmountAsString(amount) : '';
97101

98102
const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
99103
const [formError, setFormError] = useState<MaybePhraseKey>('');
@@ -135,7 +139,7 @@ function MoneyRequestAmountForm(
135139
};
136140

137141
const initializeAmount = useCallback((newAmount: number) => {
138-
const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : '';
142+
const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmountAsString(newAmount) : '';
139143
setCurrentAmount(frontendAmount);
140144
setSelection({
141145
start: frontendAmount.length,
@@ -144,13 +148,13 @@ function MoneyRequestAmountForm(
144148
}, []);
145149

146150
useEffect(() => {
147-
if (!currency || typeof amount !== 'number') {
151+
if (!currency || typeof amount !== 'number' || shouldKeepUserInput) {
148152
return;
149153
}
150154
initializeAmount(amount);
151155
// we want to re-initialize the state only when the selected tab or amount changes
152156
// eslint-disable-next-line react-hooks/exhaustive-deps
153-
}, [selectedTab, amount]);
157+
}, [selectedTab, amount, shouldKeepUserInput]);
154158

155159
/**
156160
* Sets the selection and the amount accordingly to the value passed to the input
@@ -264,13 +268,8 @@ function MoneyRequestAmountForm(
264268
return;
265269
}
266270

267-
// Update display amount string post-edit to ensure consistency with backend amount
268-
// Reference: https://github.com/Expensify/App/issues/30505
269-
const backendAmount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(currentAmount));
270-
initializeAmount(backendAmount);
271-
272271
onSubmitButtonPress({amount: currentAmount, currency});
273-
}, [currentAmount, taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount, initializeAmount]);
272+
}, [currentAmount, taxAmount, isTaxAmountForm, onSubmitButtonPress, currency, formattedTaxAmount]);
274273

275274
/**
276275
* Input handler to check for a forward-delete key (or keyboard shortcut) press.

src/types/onyx/Transaction.ts

+3
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
215215

216216
/** Indicates transaction loading */
217217
isLoading?: boolean;
218+
219+
/** Whether the user input should be kept */
220+
shouldShowOriginalAmount?: boolean;
218221
},
219222
keyof Comment
220223
>;

tests/unit/CurrencyUtilsTest.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -105,15 +105,29 @@ describe('CurrencyUtils', () => {
105105
});
106106
});
107107

108-
describe('convertToFrontendAmount', () => {
108+
describe('convertToFrontendAmountAsInteger', () => {
109109
test.each([
110110
[2500, 25],
111111
[2550, 25.5],
112112
[25, 0.25],
113113
[2500, 25],
114114
[2500.5, 25], // The backend should never send a decimal .5 value
115-
])('Correctly converts %s to amount in units handled in frontend', (amount, expectedResult) => {
116-
expect(CurrencyUtils.convertToFrontendAmount(amount)).toBe(expectedResult);
115+
])('Correctly converts %s to amount in units handled in frontend as an integer', (amount, expectedResult) => {
116+
expect(CurrencyUtils.convertToFrontendAmountAsInteger(amount)).toBe(expectedResult);
117+
});
118+
});
119+
120+
describe('convertToFrontendAmountAsString', () => {
121+
test.each([
122+
[2500, '25.00'],
123+
[2550, '25.50'],
124+
[25, '0.25'],
125+
[2500.5, '25.00'],
126+
[null, ''],
127+
[undefined, ''],
128+
[0, '0.00'],
129+
])('Correctly converts %s to amount in units handled in frontend as a string', (input, expectedResult) => {
130+
expect(CurrencyUtils.convertToFrontendAmountAsString(input)).toBe(expectedResult);
117131
});
118132
});
119133

0 commit comments

Comments
 (0)