From d99170a61a4d44c2b750123bf14d374b82869214 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 00:35:57 +0100 Subject: [PATCH 001/104] Add TS definitions --- src/types/onyx/Transaction.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 281b6b4228ce..46b60ee8b6ec 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -98,6 +98,13 @@ type TaxRate = { data?: TaxRateData; }; +type SplitShare = { + amount: number; + isModified?: boolean; +}; + +type SplitShares = Record; + type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< { /** The original transaction amount */ @@ -215,6 +222,12 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** Indicates transaction loading */ isLoading?: boolean; + + /** Holds individual shares of a split keyed by accountID, only used locally */ + splitShares?: SplitShares; + + /** Holds the accountIDs of accounts who paid the split, for now only supports a single payer */ + splitPayerAccountIDs?: number[]; }, keyof Comment >; @@ -245,4 +258,6 @@ export type { TaxRate, ReceiptSource, TransactionCollectionDataSet, + SplitShare, + SplitShares, }; From 99efbb6a82e1955db820d69106ddf7c311d5bad9 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 00:47:20 +0100 Subject: [PATCH 002/104] Set split shares when first creating the split --- src/libs/actions/IOU.ts | 22 ++++++++++++++++++- .../iou/request/step/IOURequestStepAmount.tsx | 4 ++++ .../step/IOURequestStepParticipants.tsx | 8 ++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cd0264ddb6ea..70f611ac65e9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -56,7 +56,7 @@ import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {Comment, Receipt, ReceiptSource, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; +import type {Comment, Receipt, ReceiptSource, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CachedPDFPaths from './CachedPDFPaths'; @@ -5388,6 +5388,25 @@ function setShownHoldUseExplanation() { Onyx.set(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, true); } +function resetSplitShares(transactionID: string, participantAccountIDs: number[], amount: number, currency: string) { + const participantAccountIDsWithoutCurrentUser = participantAccountIDs.filter((accountID) => accountID !== userAccountID); + const splitShares: SplitShares = [userAccountID, ...participantAccountIDsWithoutCurrentUser].reduce((result: SplitShares, accountID): SplitShares => { + const isPayer = accountID === userAccountID; + const splitAmount = IOUUtils.calculateAmount(participantAccountIDsWithoutCurrentUser.length, amount, currency, isPayer); + return { + ...result, + [accountID]: { + amount: splitAmount, + }, + }; + }, {}); + + // TODO: figure out why this needs `then` + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares: null}).then(() => { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares}); + }); +} + /** * Put money request on HOLD */ @@ -5582,6 +5601,7 @@ export { setMoneyRequestTaxAmount, setMoneyRequestTaxRate, setShownHoldUseExplanation, + resetSplitShares, updateMoneyRequestDate, updateMoneyRequestBillable, updateMoneyRequestMerchant, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 1936a132c665..f722c9e2b17e 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -127,6 +127,10 @@ function IOURequestStepAmount({ // to the confirm step. if (report?.reportID) { IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + if (isSplitBill && !report.isOwnPolicyExpenseChat && report.participants) { + const participantAccountIDs = Object.keys(report.participants).map((accountID) => Number(accountID)); + IOU.resetSplitShares(transactionID, participantAccountIDs, amountInSmallestCurrencyUnits, currency || CONST.CURRENCY.USD); + } Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index cebb000b2121..320cfb97e8f1 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -133,11 +133,17 @@ function IOURequestStepParticipants({ nextStepIOUType = CONST.IOU.TYPE.SEND; } + const isPolicyExpenseChat = participants?.some((participant) => participant.isPolicyExpenseChat); + if (nextStepIOUType === CONST.IOU.TYPE.SPLIT && !isPolicyExpenseChat) { + const participantAccountIDs = participants?.map((participant) => participant.accountID) as number[]; + IOU.resetSplitShares(transactionID, participantAccountIDs, transaction?.amount ?? 0, transaction?.currency ?? CONST.CURRENCY.USD); + } + IOU.setMoneyRequestTag(transactionID, ''); IOU.setMoneyRequestCategory(transactionID, ''); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, nextStepIOUType, transactionID, selectedReportID.current || reportID)); }, - [iouType, transactionID, reportID], + [iouType, transactionID, reportID, participants, transaction?.amount, transaction?.currency], ); const navigateBack = useCallback(() => { From cf6504400af7bf79d8464faabf42e99072db5b22 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 16 Apr 2024 03:01:04 +0100 Subject: [PATCH 003/104] Add IOU functions to set split shares and adjust them --- src/libs/actions/IOU.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 70f611ac65e9..2b7d08b1346a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5407,6 +5407,27 @@ function resetSplitShares(transactionID: string, participantAccountIDs: number[] }); } +function setSplitShare(transactionID: string, participantAccountID: number, participantShare: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + splitShares: { + [participantAccountID]: {amount: participantShare, isModified: true}, + }, + }); +} + +function adjustRemainingSplitShares(transactionID: string, remainingAccountIDs: number[], remainingAmount: number, currency: string) { + const splitShares: SplitShares = remainingAccountIDs.reduce((result: SplitShares, accountID: number, index: number): SplitShares => { + const splitAmount = IOUUtils.calculateAmount(remainingAccountIDs.length - 1, remainingAmount, currency, index === 0); + return { + ...result, + [accountID]: { + amount: splitAmount, + }, + }; + }, {}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitShares}); +} + /** * Put money request on HOLD */ @@ -5602,6 +5623,8 @@ export { setMoneyRequestTaxRate, setShownHoldUseExplanation, resetSplitShares, + setSplitShare, + adjustRemainingSplitShares, updateMoneyRequestDate, updateMoneyRequestBillable, updateMoneyRequestMerchant, From 97ac99107789143be0118508ac75ec12bf09ae80 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 17 Apr 2024 23:09:41 +0100 Subject: [PATCH 004/104] Allow configuring split amounts --- ...raryForRefactorRequestConfirmationList.tsx | 83 +++++-------------- 1 file changed, 20 insertions(+), 63 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 21815f00253b..215c6f3b0ea9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -190,7 +190,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: pickedParticipants, payeePersonalDetails, - canModifyParticipants = false, session, isReadOnly = false, bankAccountRoute = '', @@ -228,6 +227,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + console.log(transaction); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -339,17 +339,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true); }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); - }, - [iouAmount, iouCurrencyCode], - ); - // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); @@ -380,45 +369,26 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants); - useEffect(() => { - userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants; - }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]); - const shouldDisablePaidBySection = userCanModifyParticipants.current; const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipants) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + console.log(transaction?.splitShares); + console.log(transaction?.splitShares?.[payeeOption.accountID]?.amount); + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ + ...participantOption, + descriptiveText: null, + amountValue: transaction?.splitShares?.[participantOption.accountID]?.amount, + amountCurrency: iouCurrencyCode, + onAmountChange: (value) => {}, + })); - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - personalDetailsOfPayee, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + sections.push({ + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -431,18 +401,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - pickedParticipants, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - personalDetailsOfPayee, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); + }, [transaction?.splitShares, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -957,18 +916,16 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) Date: Wed, 17 Apr 2024 23:10:49 +0100 Subject: [PATCH 005/104] Use temporary amount input --- src/components/AmountTextInput.tsx | 18 +- src/components/MoneyRequestAmountInput.tsx | 206 ++++++++++++++++++ ...raryForRefactorRequestConfirmationList.tsx | 21 +- src/components/OptionRow.tsx | 10 +- .../TextInput/BaseTextInput/index.tsx | 6 +- 5 files changed, 247 insertions(+), 14 deletions(-) create mode 100644 src/components/MoneyRequestAmountInput.tsx diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index abdef6707327..ff20bbb79ac2 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -34,7 +34,19 @@ type AmountTextInputProps = { } & Pick; function AmountTextInput( - {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, ...rest}: AmountTextInputProps, + { + formattedAmount, + onChangeAmount, + placeholder, + selection, + onSelectionChange, + style, + touchableInputWrapperStyle = null, + inputStyle = null, + textInputContainerStyles, + onKeyPress, + ...rest + }: AmountTextInputProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -44,8 +56,8 @@ function AmountTextInput( autoGrow hideFocusedState shouldInterceptSwipe - inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} - textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} + inputStyle={inputStyle ?? [styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} + textInputContainerStyles={textInputContainerStyles ?? [styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} onChangeText={onChangeAmount} ref={ref} value={formattedAmount} diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx new file mode 100644 index 000000000000..806c31b1c499 --- /dev/null +++ b/src/components/MoneyRequestAmountInput.tsx @@ -0,0 +1,206 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import * as Browser from '@libs/Browser'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import type {MaybePhraseKey} from '@libs/Localize'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import CONST from '@src/CONST'; +import type {SelectedTabRequest} from '@src/types/onyx'; +import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; +import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; + +type MoneyRequestAmountFormProps = { + /** IOU amount saved in Onyx */ + amount?: number; + + /** Currency chosen by user or saved in Onyx */ + currency?: string; + + /** Whether the currency symbol is pressable */ + isCurrencyPressable?: boolean; + + hideCurrencySymbol?: boolean; + + prefixCharacter?: string; + + /** Fired when back button pressed, navigates to currency selection page */ + onCurrencyButtonPress?: () => void; + + /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ + selectedTab?: SelectedTabRequest; +}; + +type Selection = { + start: number; + end: number; +}; + +/** + * Returns the new selection object based on the updated amount's length + */ +const getNewSelection = (oldSelection: Selection, prevLength: number, newLength: number): Selection => { + const cursorPosition = oldSelection.end + (newLength - prevLength); + return {start: cursorPosition, end: cursorPosition}; +}; + +function MoneyRequestAmountTextInput( + { + amount = 0, + currency = CONST.CURRENCY.USD, + isCurrencyPressable = true, + onCurrencyButtonPress, + prefixCharacter, + hideCurrencySymbol, + inputStyle = null, + textInputContainerStyles = null, + selectedTab = CONST.TAB_REQUEST.MANUAL, + }: MoneyRequestAmountFormProps, + forwardedRef: ForwardedRef, +) { + const {toLocaleDigit, numberFormat} = useLocalize(); + const textInput = useRef(null); + + const decimals = CurrencyUtils.getCurrencyDecimals(currency); + const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : ''; + + const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString); + const [formError, setFormError] = useState(''); + + const [selection, setSelection] = useState({ + start: selectedAmountAsString.length, + end: selectedAmountAsString.length, + }); + + const forwardDeletePressedRef = useRef(false); + + const initializeAmount = useCallback((newAmount: number) => { + const frontendAmount = newAmount ? CurrencyUtils.convertToFrontendAmount(newAmount).toString() : ''; + setCurrentAmount(frontendAmount); + setSelection({ + start: frontendAmount.length, + end: frontendAmount.length, + }); + }, []); + + useEffect(() => { + if (!currency || typeof amount !== 'number') { + return; + } + initializeAmount(amount); + // we want to re-initialize the state only when the amount changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [amount]); + + /** + * Sets the selection and the amount accordingly to the value passed to the input + * @param {String} newAmount - Changed amount from user input + */ + const setNewAmount = useCallback( + (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); + // Use a shallow copy of selection to trigger setSelection + // More info: https://github.com/Expensify/App/issues/16385 + if (!MoneyRequestUtils.validateAmount(newAmountWithoutSpaces, decimals)) { + setSelection((prevSelection) => ({...prevSelection})); + return; + } + if (formError) { + setFormError(''); + } + + // setCurrentAmount contains another setState(setSelection) making it error-prone since it is leading to setSelection being called twice for a single setCurrentAmount call. This solution introducing the hasSelectionBeenSet flag was chosen for its simplicity and lower risk of future errors https://github.com/Expensify/App/issues/23300#issuecomment-1766314724. + + let hasSelectionBeenSet = false; + setCurrentAmount((prevAmount) => { + const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces); + const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current; + if (!hasSelectionBeenSet) { + hasSelectionBeenSet = true; + setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); + } + return strippedAmount; + }); + }, + [decimals, formError], + ); + + useEffect(() => {}); + + // 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)) { + return; + } + + // If the changed currency doesn't support decimals, we can strip the decimals + setNewAmount(MoneyRequestUtils.stripDecimalsFromAmount(currentAmount)); + + // we want to update only when decimals change (setNewAmount also changes when decimals change). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setNewAmount]); + + /** + * Input handler to check for a forward-delete key (or keyboard shortcut) press. + */ + const textInputKeyPress = ({nativeEvent}: NativeSyntheticEvent) => { + const key = nativeEvent?.key.toLowerCase(); + if (Browser.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; + return; + } + // Control-D on Mac is a keyboard shortcut for forward-delete. See https://support.apple.com/en-us/HT201236 for Mac keyboard shortcuts. + // Also check for the keyboard shortcut on iOS in cases where a hardware keyboard may be connected to the device. + const operatingSystem = getOperatingSystem(); + forwardDeletePressedRef.current = key === 'delete' || ((operatingSystem === CONST.OS.MAC_OS || operatingSystem === CONST.OS.IOS) && nativeEvent?.ctrlKey && key === 'd'); + }; + + const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); + + useEffect(() => { + setFormError(''); + }, [selectedTab]); + + return ( + { + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } else if (forwardedRef?.current) { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = ref; + } + textInput.current = ref; + }} + selectedCurrencyCode={currency} + selection={selection} + onSelectionChange={(e: NativeSyntheticEvent) => { + const maxSelection = formattedAmount.length; + const start = Math.min(e.nativeEvent.selection.start, maxSelection); + const end = Math.min(e.nativeEvent.selection.end, maxSelection); + setSelection({start, end}); + }} + onKeyPress={textInputKeyPress} + isCurrencyPressable={isCurrencyPressable} + hideCurrencySymbol={hideCurrencySymbol} + prefixCharacter={prefixCharacter} + inputStyle={inputStyle} + textInputContainerStyles={textInputContainerStyles} + /> + ); +} + +MoneyRequestAmountTextInput.displayName = 'MoneyRequestAmountTextInput'; + +export default React.forwardRef(MoneyRequestAmountTextInput); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 215c6f3b0ea9..3f3826d14ca3 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -190,6 +190,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: pickedParticipants, payeePersonalDetails, + currencyList, session, isReadOnly = false, bankAccountRoute = '', @@ -227,7 +228,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // A flag for showing the categories field const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - console.log(transaction); // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); @@ -374,14 +374,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - console.log(transaction?.splitShares); - console.log(transaction?.splitShares?.[payeeOption.accountID]?.amount); const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ ...participantOption, descriptiveText: null, - amountValue: transaction?.splitShares?.[participantOption.accountID]?.amount, - amountCurrency: iouCurrencyCode, - onAmountChange: (value) => {}, + amountProps: { + value: transaction?.splitShares?.[participantOption.accountID]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, + isCurrencyPressable: false, + hideCurrencySymbol: true, + style: [{minWidth: 100}], + onAmountChange: (value) => {}, + }, })); sections.push({ @@ -401,7 +405,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [transaction?.splitShares, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -920,8 +924,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onAddToSelection={selectParticipant} onConfirmSelection={confirm} selectedOptions={selectedOptions} - disableArrowKeysActions - boldStyle showTitleTooltip shouldTextInputAppearBelowOptions shouldShowTextInput={false} @@ -1007,4 +1009,5 @@ export default withOnyx `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, })(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 90ccff47b2b9..acd4c8a9d4d2 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -15,12 +15,14 @@ import DisplayNames from './DisplayNames'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import MoneyRequestAmountInput from './MoneyRequestAmountInput'; import MultipleAvatars from './MultipleAvatars'; import OfflineWithFeedback from './OfflineWithFeedback'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SelectCircle from './SelectCircle'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; +import TextInput from './TextInput'; type OptionRowProps = { /** Style for hovered state */ @@ -251,6 +253,11 @@ function OptionRow({ {option.descriptiveText} ) : null} + {option.amountProps ? ( + + + + ) : null} {!isSelected && option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( { + // TODO: figure out padding for single and 3 character currencies + return 10; switch (prefix) { + case CONST.CURRENCY.USD: case CONST.POLICY.ROOM_PREFIX: return 10; default: @@ -342,7 +346,7 @@ function BaseTextInput( {prefixCharacter} From e9cc173fd234c82a6623df60e766e710aad02153 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Wed, 17 Apr 2024 23:50:51 +0100 Subject: [PATCH 006/104] Pass custom styles --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 3f3826d14ca3..973028ada3dd 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -378,12 +378,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participantOption, descriptiveText: null, amountProps: { - value: transaction?.splitShares?.[participantOption.accountID]?.amount, + amount: transaction?.splitShares?.[participantOption.accountID]?.amount, currency: iouCurrencyCode, prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - style: [{minWidth: 100}], + inputStyle: [{width: 100}], + textInputContainerStyles: [], + prefixStyle: [{paddingTop: 0, paddingBottom: 0}], onAmountChange: (value) => {}, }, })); From b24ff87479e0bc8818fe41241abb8b55bbcde871 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 17:04:18 +0100 Subject: [PATCH 007/104] Temporary updates to the amount input --- src/components/AmountTextInput.tsx | 18 +++--------------- ...oraryForRefactorRequestConfirmationList.tsx | 3 --- src/components/OptionRow.tsx | 6 +----- .../TextInput/BaseTextInput/index.tsx | 6 ++++-- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index ff20bbb79ac2..abdef6707327 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -34,19 +34,7 @@ type AmountTextInputProps = { } & Pick; function AmountTextInput( - { - formattedAmount, - onChangeAmount, - placeholder, - selection, - onSelectionChange, - style, - touchableInputWrapperStyle = null, - inputStyle = null, - textInputContainerStyles, - onKeyPress, - ...rest - }: AmountTextInputProps, + {formattedAmount, onChangeAmount, placeholder, selection, onSelectionChange, style, touchableInputWrapperStyle, onKeyPress, ...rest}: AmountTextInputProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -56,8 +44,8 @@ function AmountTextInput( autoGrow hideFocusedState shouldInterceptSwipe - inputStyle={inputStyle ?? [styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} - textInputContainerStyles={textInputContainerStyles ?? [styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} + inputStyle={[styles.iouAmountTextInput, styles.p0, styles.noLeftBorderRadius, styles.noRightBorderRadius, style]} + textInputContainerStyles={[styles.borderNone, styles.noLeftBorderRadius, styles.noRightBorderRadius]} onChangeText={onChangeAmount} ref={ref} value={formattedAmount} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 973028ada3dd..f40560be3f5a 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -383,9 +383,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - inputStyle: [{width: 100}], - textInputContainerStyles: [], - prefixStyle: [{paddingTop: 0, paddingBottom: 0}], onAmountChange: (value) => {}, }, })); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index acd4c8a9d4d2..2827f4fa7173 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -253,11 +253,7 @@ function OptionRow({ {option.descriptiveText} ) : null} - {option.amountProps ? ( - - - - ) : null} + {option.amountProps ? : null} {!isSelected && option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, - autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, + autoGrow && StyleUtils.getWidthStyle(autoGrowWidth), + {minWidth: autoGrowWidth}, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); @@ -346,7 +348,7 @@ function BaseTextInput( {prefixCharacter} From 9e53204f30426c4ac5067e8b145f5a2e2b958676 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:36:09 +0100 Subject: [PATCH 008/104] Allow setting individual shares --- src/components/MoneyRequestAmountInput.tsx | 4 +++- ...poraryForRefactorRequestConfirmationList.tsx | 17 +++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 806c31b1c499..7d170f78d073 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -53,6 +53,7 @@ function MoneyRequestAmountTextInput( isCurrencyPressable = true, onCurrencyButtonPress, prefixCharacter, + onAmountChange, hideCurrencySymbol, inputStyle = null, textInputContainerStyles = null, @@ -123,10 +124,11 @@ function MoneyRequestAmountTextInput( hasSelectionBeenSet = true; setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedAmount.length : prevAmount.length, strippedAmount.length)); } + onAmountChange?.(strippedAmount); return strippedAmount; }); }, - [decimals, formError], + [decimals, formError, onAmountChange], ); useEffect(() => {}); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 6610b9fda2e2..be47497512d1 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -115,9 +115,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Payee of the expense with login */ payeePersonalDetails?: OnyxTypes.PersonalDetails; - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - /** Should the list be read only, and not editable? */ isReadOnly?: boolean; @@ -374,11 +371,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const onSplitShareChange = useCallback( + (accountID: number, value) => { + const amountInCents = CurrencyUtils.convertToBackendAmount(value); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents, iouCurrencyCode, true); + }, + [transaction, iouCurrencyCode], + ); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption) => ({ + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ ...participantOption, descriptiveText: null, amountProps: { @@ -387,7 +392,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, hideCurrencySymbol: true, - onAmountChange: (value) => {}, + onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), }, })); @@ -408,7 +413,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [transaction?.splitShares, onSplitShareChange, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { From 00db6ffcc34d024c820d98426f403282ef4eaa9e Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:52:34 +0100 Subject: [PATCH 009/104] Show a form error if sum of shares don't match total --- ...oraryForRefactorRequestConfirmationList.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index be47497512d1..f001e3c6452c 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -36,6 +36,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {SplitShare, SplitShares} from '@src/types/onyx/Transaction'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; @@ -379,6 +380,23 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ [transaction, iouCurrencyCode], ); + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const splitSharesMap: SplitShares = transaction.splitShares; + const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); + const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); + if (sumOfShares !== iouAmount) { + setFormError( + `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, + ); + } else { + setFormError(''); + } + }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { From 23d8693339a4ae3078f3d93051bf042c11f24b4e Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 18:59:21 +0100 Subject: [PATCH 010/104] Prevent submitting form if total doesn't match --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index f001e3c6452c..9b1db9f52b69 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -550,6 +550,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } + if (formError) { + return; + } + playSound(SOUNDS.DONE); setDidConfirm(true); onConfirm?.(selectedParticipants); @@ -568,6 +572,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isDistanceRequestWithPendingRoute, iouAmount, isEditingSplitBill, + formError, onConfirm, ], ); From cc3de1411c101acd7eba3171f570845b1e615b17 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 20:25:11 +0100 Subject: [PATCH 011/104] Adjust shares automatically --- ...raryForRefactorRequestConfirmationList.tsx | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 9b1db9f52b69..93f1a6e4310d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -7,6 +7,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -36,7 +37,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type {SplitShare, SplitShares} from '@src/types/onyx/Transaction'; +import type {SplitShares} from '@src/types/onyx/Transaction'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; @@ -373,11 +374,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); const onSplitShareChange = useCallback( - (accountID: number, value) => { + (accountID: number, value: number) => { + if (!transaction?.transactionID) { + return; + } const amountInCents = CurrencyUtils.convertToBackendAmount(value); - IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents, iouCurrencyCode, true); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); }, - [transaction, iouCurrencyCode], + [transaction?.transactionID], ); useEffect(() => { @@ -397,6 +401,31 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const sumOfManualShares = Object.keys(transaction.splitShares) + .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) + .reduce((prev: number, current: number): number => prev + current, 0); + + if (!sumOfManualShares) { + return; + } + + const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) + .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string) => Number(key)); + + const remainingTotal = iouAmount - sumOfManualShares; + if (remainingTotal <= 0) { + return; + } + IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); + }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { @@ -405,7 +434,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participantOption, descriptiveText: null, amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID]?.amount, + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, currency: iouCurrencyCode, prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, isCurrencyPressable: false, From d98103f6e391d109bcbe5e39996180296c9dbae0 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 20:27:24 +0100 Subject: [PATCH 012/104] Revert changes --- src/components/TextInput/BaseTextInput/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 36214711287f..519a52fd85ec 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -59,7 +59,6 @@ function BaseTextInput( shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter = '', - prefixStyle = [], inputID, isMarkdownEnabled = false, ...inputProps @@ -242,10 +241,7 @@ function BaseTextInput( // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. const getCharacterPadding = (prefix: string): number => { - // TODO: figure out padding for single and 3 character currencies - return 10; switch (prefix) { - case CONST.CURRENCY.USD: case CONST.POLICY.ROOM_PREFIX: return 10; default: @@ -260,13 +256,11 @@ function BaseTextInput( const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const autoGrowWidth = textInputWidth + getCharacterPadding(prefixCharacter) + 15; const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, - autoGrow && StyleUtils.getWidthStyle(autoGrowWidth), - {minWidth: autoGrowWidth}, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); From 14c9ead507b1b93d5e9734f8cb74e9e3dd481f6f Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 21:57:07 +0100 Subject: [PATCH 013/104] Use temporary input --- ...emporaryForRefactorRequestConfirmationList.tsx | 7 ++++++- src/components/TextInput/BaseTextInput/index.tsx | 15 +-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 93f1a6e4310d..896ad130bca9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -11,6 +11,7 @@ import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -211,6 +212,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canUseViolations} = usePermissions(); @@ -430,15 +432,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const sections = []; if (hasMultipleParticipants) { const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ ...participantOption, descriptiveText: null, amountProps: { amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, currency: iouCurrencyCode, - prefixCharacter: currencyList?.[iouCurrencyCode]?.symbol ?? iouCurrencyCode, + prefixCharacter: currencySymbol, isCurrencyPressable: false, hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), }, })); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 519a52fd85ec..5d1fa4e63742 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -234,20 +234,7 @@ function BaseTextInput( setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); - // When adding a new prefix character, adjust this method to add expected character width. - // This is because character width isn't known before it's rendered to the screen, and once it's rendered, - // it's too late to calculate it's width because the change in padding would cause a visible jump. - // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size - // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, - // this method will produce reliable results. - const getCharacterPadding = (prefix: string): number => { - switch (prefix) { - case CONST.POLICY.ROOM_PREFIX: - return 10; - default: - throw new Error(`Prefix ${prefix} has no padding assigned.`); - } - }; + const getCharacterPadding = (prefix: string): number => prefix.length * 10; const hasLabel = Boolean(label?.length); const isReadOnly = inputProps.readOnly ?? inputProps.disabled; From f032df020db2fdd9452cbff4a5b259f3b8f019f2 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 22:10:20 +0100 Subject: [PATCH 014/104] Show even splits for workspace chat splits --- ...raryForRefactorRequestConfirmationList.tsx | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 896ad130bca9..1e86d0598d17 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -212,7 +212,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {canUseViolations} = usePermissions(); @@ -428,26 +427,38 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const getParticipantOptions = useCallback(() => { + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); + if (isPolicyExpenseChat) { + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { + const isPayer = participantOption.accountID === payeeOption.accountID; + const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); + return { + ...participantOption, + descriptiveText: CurrencyUtils.convertToDisplayString(amount), + }; + }); + } + + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ + ...participantOption, + amountProps: { + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencySymbol, + isCurrencyPressable: false, + hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], + onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), + }, + })); + }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, personalDetailsOfPayee, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); + const optionSelectorSections = useMemo(() => { const sections = []; if (hasMultipleParticipants) { - const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; - - const formattedParticipantsList = [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ - ...participantOption, - descriptiveText: null, - amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, - currency: iouCurrencyCode, - prefixCharacter: currencySymbol, - isCurrencyPressable: false, - hideCurrencySymbol: true, - textInputContainerStyles: [{minWidth: 50}], - onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), - }, - })); - + const formattedParticipantsList = getParticipantOptions(); sections.push({ title: translate('moneyRequestConfirmationList.splitWith'), data: formattedParticipantsList, @@ -465,7 +476,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [transaction?.splitShares, onSplitShareChange, currencyList, selectedParticipants, hasMultipleParticipants, iouCurrencyCode, personalDetailsOfPayee, translate]); + }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { From 3d2e12e0d5ac3769838d4f70f96a1271b90c5c15 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 22:38:57 +0100 Subject: [PATCH 015/104] Send uneven splits to backend --- src/libs/actions/IOU.ts | 12 ++++++++++-- src/pages/iou/request/step/IOURequestStepAmount.tsx | 1 + .../iou/request/step/IOURequestStepConfirmation.tsx | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 10dc1b44fde5..37daebcd01d6 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3143,6 +3143,7 @@ function createSplitsAndOnyxData( created: string, category: string, tag: string, + splitShares: SplitShares = {}, existingSplitChatReportID = '', billable = false, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, @@ -3311,13 +3312,15 @@ function createSplitsAndOnyxData( } // Loop through participants creating individual chats, iouReports and reportActionIDs as needed - const splitAmount = IOUUtils.calculateAmount(participants.length, amount, currency, false); - const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: IOUUtils.calculateAmount(participants.length, amount, currency, true)}]; + const currentUserAmount = isOwnPolicyExpenseChat ? IOUUtils.calculateAmount(participants.length, amount, currency, true) : splitShares[currentUserAccountID].amount; + + const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: currentUserAmount}]; const hasMultipleParticipants = participants.length > 1; participants.forEach((participant) => { // In a case when a participant is a workspace, even when a current user is not an owner of the workspace const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant); + const splitAmount = isPolicyExpenseChat ? IOUUtils.calculateAmount(participants.length, amount, currency, false) : splitShares[participant.accountID ?? 0].amount; // In case the participant is a workspace, email & accountID should remain undefined and won't be used in the rest of this code // participant.login is undefined when the request is initiated from a group DM with an unknown user, so we need to add a default @@ -3507,6 +3510,7 @@ type SplitBillActionsParams = { billable?: boolean; iouRequestType?: IOURequestType; existingSplitChatReportID?: string; + splitShares?: SplitShares; }; /** @@ -3527,6 +3531,7 @@ function splitBill({ billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, existingSplitChatReportID = '', + splitShares = {}, }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3540,6 +3545,7 @@ function splitBill({ currentCreated, category, tag, + splitShares, existingSplitChatReportID, billable, iouRequestType, @@ -3586,6 +3592,7 @@ function splitBillAndOpenReport({ tag = '', billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + splitShares = {}, }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3599,6 +3606,7 @@ function splitBillAndOpenReport({ currentCreated, category, tag, + splitShares, '', billable, iouRequestType, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 752f291642cd..12dc29418e93 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -170,6 +170,7 @@ function IOURequestStepAmount({ created: transaction?.created ?? '', billable: false, iouRequestType: CONST.IOU.REQUEST_TYPE.MANUAL, + splitShares: transaction?.splitShares, }); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1d84bff8747b..71b0d369912f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -326,6 +326,7 @@ function IOURequestStepConfirmation({ existingSplitChatReportID: report?.reportID, billable: transaction.billable, iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, }); } return; @@ -347,6 +348,7 @@ function IOURequestStepConfirmation({ tag: transaction.tag, billable: !!transaction.billable, iouRequestType: transaction.iouRequestType, + splitShares: transaction.splitShares, }); } return; From eb5c8f12ad5776ef8c5e99252b27fe1eab05938a Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Thu, 18 Apr 2024 23:12:24 +0100 Subject: [PATCH 016/104] Save splits array in the transaction optimistically --- src/libs/actions/IOU.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 37daebcd01d6..fc200a8768f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3477,6 +3477,16 @@ function createSplitsAndOnyxData( failureData.push(...oneOnOneFailureData); }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`, + value: { + comment: { + splits: splits.map((split) => ({accountID: split.accountID, amount: split.amount})), + }, + }, + }); + const splitData: SplitData = { chatReportID: splitChatReport.reportID, transactionID: splitTransaction.transactionID, From 20018215ab1a2ffba7e2228fc323f4104eaed68b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 19 Apr 2024 23:16:50 +0100 Subject: [PATCH 017/104] Copy changes to MoneyRequestConfirmationList --- .../MoneyRequestConfirmationList.tsx | 158 ++- ...raryForRefactorRequestConfirmationList.tsx | 1047 ----------------- 2 files changed, 95 insertions(+), 1110 deletions(-) delete mode 100755 src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e309df1ab654..a146332f80c8 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -37,6 +37,7 @@ import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {SplitShares} from '@src/types/onyx/Transaction'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; @@ -121,9 +122,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Payee of the expense with login */ payeePersonalDetails?: OnyxEntry; - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - /** Should the list be read only, and not editable? */ isReadOnly?: boolean; @@ -171,6 +169,8 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** The action to take */ action?: IOUAction; + + currencyList: OnyxEntry; }; const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { @@ -200,7 +200,6 @@ function MoneyRequestConfirmationList({ hasMultipleParticipants, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, - canModifyParticipants: canModifyParticipantsProp = false, session, isReadOnly = false, bankAccountRoute = '', @@ -218,6 +217,7 @@ function MoneyRequestConfirmationList({ defaultMileageRate, lastSelectedDistanceRates, action = CONST.IOU.ACTION.CREATE, + currencyList, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -374,17 +374,6 @@ function MoneyRequestConfirmationList({ IOU.setMoneyRequestTaxAmount(transactionID, amountInSmallestCurrencyUnits, true); }, [taxRates?.defaultValue, transaction, transactionID, previousTransactionAmount]); - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); - }, - [iouAmount, iouCurrencyCode], - ); - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); @@ -413,43 +402,98 @@ function MoneyRequestConfirmationList({ ]; }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); + const onSplitShareChange = useCallback( + (accountID: number, value: number) => { + if (!transaction?.transactionID) { + return; + } + const amountInCents = CurrencyUtils.convertToBackendAmount(value); + IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); + }, + [transaction?.transactionID], + ); + + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const splitSharesMap: SplitShares = transaction.splitShares; + const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); + const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); + if (sumOfShares !== iouAmount) { + setFormError( + `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, + ); + } else { + setFormError(''); + } + }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); + + useEffect(() => { + if (!isTypeSplit || !transaction?.splitShares) { + return; + } + + const sumOfManualShares = Object.keys(transaction.splitShares) + .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) + .reduce((prev: number, current: number): number => prev + current, 0); + + if (!sumOfManualShares) { + return; + } + + const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) + .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) + .map((key: string) => Number(key)); + + const remainingTotal = iouAmount - sumOfManualShares; + if (remainingTotal <= 0) { + return; + } + IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); + }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); + const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); - const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; + const getParticipantOptions = useCallback(() => { + const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails); + if (isPolicyExpenseChat) { + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { + const isPayer = participantOption.accountID === payeeOption.accountID; + const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); + return { + ...participantOption, + descriptiveText: CurrencyUtils.convertToDisplayString(amount), + }; + }); + } + + const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; + return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ + ...participantOption, + amountProps: { + amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, + currency: iouCurrencyCode, + prefixCharacter: currencySymbol, + isCurrencyPressable: false, + hideCurrencySymbol: true, + textInputContainerStyles: [{minWidth: 50}], + onAmountChange: (value: string) => onSplitShareChange(participantOption.accountID ?? 0, Number(value)), + }, + })); + }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, payeePersonalDetails, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); + const optionSelectorSections = useMemo(() => { const sections = []; - const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipants) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } - - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + const formattedParticipantsList = getParticipantOptions(); + sections.push({ + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -462,18 +506,7 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - selectedParticipantsProp, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - payeePersonalDetails, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); + }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -982,18 +1015,16 @@ function MoneyRequestConfirmationList({ // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) ; - - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; - - /** The policy of the report */ - policy: OnyxEntry; - - /** The session of the logged in user */ - session: OnyxEntry; - - /** Unit and rate used for if the expense is a distance expense */ - mileageRate: OnyxEntry; -}; - -type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { - /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Participant[]) => void; - - /** Callback to parent modal to pay someone */ - onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void; - - /** Callback to inform a participant is selected */ - onSelectParticipant?: (option: Participant) => void; - - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: boolean; - - /** IOU amount */ - iouAmount: number; - - /** IOU comment */ - iouComment?: string; - - /** IOU currency */ - iouCurrencyCode?: string; - - /** IOU type */ - iouType?: IOUType; - - /** IOU date */ - iouCreated?: string; - - /** IOU merchant */ - iouMerchant?: string; - - /** IOU Category */ - iouCategory?: string; - - /** IOU isBillable */ - iouIsBillable?: boolean; - - /** Callback to toggle the billable state */ - onToggleBillable?: (isOn: boolean) => void; - - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Participant[]; - - /** Payee of the expense with login */ - payeePersonalDetails?: OnyxTypes.PersonalDetails; - - /** Should the list be read only, and not editable? */ - isReadOnly?: boolean; - - /** Depending on expense report or personal IOU report, respective bank account route */ - bankAccountRoute?: Route; - - /** The policyID of the request */ - policyID?: string; - - /** The reportID of the request */ - reportID?: string; - - /** File path of the receipt */ - receiptPath?: string; - - /** File name of the receipt */ - receiptFilename?: string; - - /** List styles for OptionsSelector */ - listStyles?: StyleProp; - - /** Transaction that represents the expense */ - transaction?: OnyxEntry; - - /** Whether the expense is a distance expense */ - isDistanceRequest?: boolean; - - /** Whether the expense is a scan expense */ - isScanRequest?: boolean; - - /** Whether we're editing a split expense */ - isEditingSplitBill?: boolean; - - /** Whether we should show the amount, date, and merchant fields. */ - shouldShowSmartScanFields?: boolean; - - /** A flag for verifying that the current report is a sub-report of a workspace chat */ - isPolicyExpenseChat?: boolean; - - /** Whether smart scan failed */ - hasSmartScanFailed?: boolean; - - reportActionID?: string; - - action?: IOUAction; -}; - -const getTaxAmount = (transaction: OnyxEntry, defaultTaxValue: string) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const percentage = (transaction?.taxRate ? transaction?.taxRate?.data?.value : defaultTaxValue) || ''; - return TransactionUtils.calculateTaxAmount(percentage, transaction?.amount ?? 0); -}; - -function MoneyTemporaryForRefactorRequestConfirmationList({ - transaction = null, - onSendMoney, - onConfirm, - onSelectParticipant, - iouType = CONST.IOU.TYPE.REQUEST, - isScanRequest = false, - iouAmount, - policyCategories, - mileageRate, - isDistanceRequest = false, - policy, - isPolicyExpenseChat = false, - iouCategory = '', - shouldShowSmartScanFields = true, - isEditingSplitBill, - policyTags, - iouCurrencyCode, - iouMerchant, - hasMultipleParticipants, - selectedParticipants: pickedParticipants, - payeePersonalDetails, - currencyList, - session, - isReadOnly = false, - bankAccountRoute = '', - policyID = '', - reportID = '', - receiptPath = '', - iouComment, - receiptFilename = '', - listStyles, - iouCreated, - iouIsBillable = false, - onToggleBillable, - hasSmartScanFailed, - reportActionID, - action = CONST.IOU.ACTION.CREATE, -}: MoneyRequestConfirmationListProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate, toLocaleDigit} = useLocalize(); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const {canUseViolations} = usePermissions(); - - const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; - - const {unit, rate, currency} = mileageRate ?? { - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - rate: 0, - currency: 'USD', - }; - const distance = transaction?.routes?.route0.distance ?? 0; - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; - const taxRates = policy?.taxRates; - - // A flag for showing the categories field - const shouldShowCategories = isPolicyExpenseChat && (!!iouCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); - - // A flag and a toggler for showing the rest of the form fields - const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - - // Do not hide fields in case of paying someone - const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - - const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; - const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; - - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - - // A flag for showing the tags field - const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); - - // A flag for showing tax rate - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy); - - // A flag for showing the billable field - const shouldShowBillable = policy?.disabledFields?.defaultBillable === false; - const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); - const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; - const formattedAmount = isDistanceRequestWithPendingRoute - ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0) : iouAmount, - isDistanceRequest ? currency : iouCurrencyCode, - ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - const taxRateTitle = taxRates && transaction ? TransactionUtils.getDefaultTaxName(taxRates, transaction) : ''; - - const previousTransactionAmount = usePrevious(transaction?.amount); - - const isFocused = useIsFocused(); - const [formError, setFormError] = useState(''); - - const [didConfirm, setDidConfirm] = useState(false); - const [didConfirmSplit, setDidConfirmSplit] = useState(false); - - const [merchantError, setMerchantError] = useState(false); - - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - - const navigateBack = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction?.transactionID ?? '', reportID)); - }; - - const shouldDisplayFieldError: boolean = useMemo(() => { - if (!isEditingSplitBill) { - return false; - } - - return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); - }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - - const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; - - const isCategoryRequired = canUseViolations && !!policy?.requiresCategory; - - useEffect(() => { - if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { - return; - } - if (!isMerchantEmpty && merchantError) { - setMerchantError(false); - if (formError === 'iou.error.invalidMerchant') { - setFormError(''); - } - } - }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); - - useEffect(() => { - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (merchantError) { - setFormError('iou.error.invalidMerchant'); - return; - } - // reset the form error whenever the screen gains or loses focus - setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); - - useEffect(() => { - if (!shouldCalculateDistanceAmount) { - return; - } - - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate ?? 0); - IOU.setMoneyRequestAmount_temporaryForRefactor(transaction?.transactionID ?? '', amount, currency ?? ''); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); - - // Calculate and set tax amount in transaction draft - useEffect(() => { - const taxAmount = getTaxAmount(transaction, taxRates?.defaultValue ?? '').toString(); - const amountInSmallestCurrencyUnits = CurrencyUtils.convertToBackendAmount(Number.parseFloat(taxAmount)); - - if (transaction?.taxAmount && previousTransactionAmount === transaction?.amount) { - return IOU.setMoneyRequestTaxAmount(transaction?.transactionID, transaction?.taxAmount, true); - } - - IOU.setMoneyRequestTaxAmount(transaction?.transactionID ?? '', amountInSmallestCurrencyUnits, true); - }, [taxRates?.defaultValue, transaction, previousTransactionAmount]); - - // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again - if (isEditingSplitBill && didConfirm) { - setDidConfirm(false); - } - - const splitOrRequestOptions: Array> = useMemo(() => { - let text; - if (isTypeTrackExpense) { - text = translate('iou.trackExpense'); - } else if (isTypeSplit && iouAmount === 0) { - text = translate('iou.splitExpense'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.submitExpense'); - if (iouAmount !== 0) { - text = translate('iou.submitAmount', {amount: formattedAmount}); - } - } else { - const translationKey = isTypeSplit ? 'iou.splitAmount' : 'iou.submitAmount'; - text = translate(translationKey, {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: iouType, - }, - ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - - const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); - const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - - const onSplitShareChange = useCallback( - (accountID: number, value: number) => { - if (!transaction?.transactionID) { - return; - } - const amountInCents = CurrencyUtils.convertToBackendAmount(value); - IOU.setSplitShare(transaction?.transactionID, accountID, amountInCents); - }, - [transaction?.transactionID], - ); - - useEffect(() => { - if (!isTypeSplit || !transaction?.splitShares) { - return; - } - - const splitSharesMap: SplitShares = transaction.splitShares; - const shares: number[] = Object.values(splitSharesMap).map((splitShare) => splitShare.amount); - const sumOfShares = shares?.reduce((prev, current): number => prev + current, 0); - if (sumOfShares !== iouAmount) { - setFormError( - `You entered ${CurrencyUtils.convertToDisplayString(sumOfShares, iouCurrencyCode)} but the total is ${CurrencyUtils.convertToDisplayString(iouAmount, iouCurrencyCode)}`, - ); - } else { - setFormError(''); - } - }, [isTypeSplit, transaction?.splitShares, iouAmount, iouCurrencyCode]); - - useEffect(() => { - if (!isTypeSplit || !transaction?.splitShares) { - return; - } - - const sumOfManualShares = Object.keys(transaction.splitShares) - .filter((key: string) => transaction?.splitShares?.[Number(key)]?.isModified) - .map((key: string): number => transaction?.splitShares?.[Number(key)]?.amount ?? 0) - .reduce((prev: number, current: number): number => prev + current, 0); - - if (!sumOfManualShares) { - return; - } - - const unModifiedSharesAccountIDs = Object.keys(transaction.splitShares) - .filter((key: string) => !transaction?.splitShares?.[Number(key)]?.isModified) - .map((key: string) => Number(key)); - - const remainingTotal = iouAmount - sumOfManualShares; - if (remainingTotal <= 0) { - return; - } - IOU.adjustRemainingSplitShares(transaction.transactionID, unModifiedSharesAccountIDs, remainingTotal, iouCurrencyCode ?? CONST.CURRENCY.USD); - }, [transaction, iouAmount, iouCurrencyCode, isTypeSplit]); - - const getParticipantOptions = useCallback(() => { - const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee); - if (isPolicyExpenseChat) { - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { - const isPayer = participantOption.accountID === payeeOption.accountID; - const amount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); - return { - ...participantOption, - descriptiveText: CurrencyUtils.convertToDisplayString(amount), - }; - }); - } - - const currencySymbol = currencyList?.[iouCurrencyCode ?? '']?.symbol ?? iouCurrencyCode; - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ - ...participantOption, - amountProps: { - amount: transaction?.splitShares?.[participantOption.accountID ?? 0]?.amount, - currency: iouCurrencyCode, - prefixCharacter: currencySymbol, - isCurrencyPressable: false, - hideCurrencySymbol: true, - textInputContainerStyles: [{minWidth: 50}], - onAmountChange: (value) => onSplitShareChange(participantOption.accountID, value), - }, - })); - }, [iouCurrencyCode, isPolicyExpenseChat, onSplitShareChange, personalDetailsOfPayee, selectedParticipants, transaction?.splitShares, currencyList, iouAmount]); - - const optionSelectorSections = useMemo(() => { - const sections = []; - if (hasMultipleParticipants) { - const formattedParticipantsList = getParticipantOptions(); - sections.push({ - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }); - } else { - const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, - isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - sections.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - shouldShow: true, - }); - } - return sections; - }, [selectedParticipants, hasMultipleParticipants, translate, getParticipantOptions]); - - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)]; - }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]); - - useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { - return; - } - - /* - Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: - When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. - In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. - */ - IOU.setMoneyRequestPendingFields(transaction?.transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate ?? 0, currency ?? 'USD', translate, toLocaleDigit); - IOU.setMoneyRequestMerchant(transaction?.transactionID ?? '', distanceMerchant, true); - }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction, action, isMovingTransactionFromTrackExpense]); - - // Auto select the category if there is only one enabled category and it is required - useEffect(() => { - const enabledCategories = Object.values(policyCategories ?? {}).filter((category) => category.enabled); - if (iouCategory || !shouldShowCategories || enabledCategories.length !== 1 || !isCategoryRequired) { - return; - } - IOU.setMoneyRequestCategory(transaction?.transactionID ?? '', enabledCategories[0].name); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [shouldShowCategories, policyCategories, isCategoryRequired]); - - // Auto select the tag if there is only one enabled tag and it is required - useEffect(() => { - let updatedTagsString = TransactionUtils.getTag(transaction); - policyTagLists.forEach((tagList, index) => { - const enabledTags = Object.values(tagList.tags).filter((tag) => tag.enabled); - const isTagListRequired = tagList.required === undefined ? false : tagList.required && canUseViolations; - if (!isTagListRequired || enabledTags.length !== 1 || TransactionUtils.getTag(transaction, index)) { - return; - } - updatedTagsString = IOUUtils.insertTagIntoTransactionTagsString(updatedTagsString, enabledTags[0] ? enabledTags[0].name : '', index); - }); - if (updatedTagsString !== TransactionUtils.getTag(transaction) && updatedTagsString) { - IOU.setMoneyRequestTag(transaction?.transactionID ?? '', updatedTagsString); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [policyTagLists, policyTags, canUseViolations]); - - /** - */ - const selectParticipant = useCallback( - (option: Participant) => { - // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID) { - return; - } - onSelectParticipant?.(option); - }, - [session?.accountID, onSelectParticipant], - ); - - /** - * Navigate to report details or profile of selected user - */ - const navigateToReportOrUserDetail = (option: ReportUtils.OptionData) => { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - if (option.isSelfDM) { - Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); - return; - } - - if (option.accountID) { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if (option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID, activeRoute)); - } - }; - - /** - * @param {String} paymentMethod - */ - const confirm = useCallback( - (paymentMethod: PaymentMethodType | undefined) => { - if (selectedParticipants.length === 0) { - return; - } - if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null))) { - setMerchantError(true); - return; - } - - if (iouType === CONST.IOU.TYPE.SEND) { - if (!paymentMethod) { - return; - } - - setDidConfirm(true); - - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney?.(paymentMethod); - } else { - // validate the amount for distance expenses - const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); - if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { - setFormError('common.error.invalidAmount'); - return; - } - - if (isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)) { - setDidConfirmSplit(true); - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - - if (formError) { - return; - } - - playSound(SOUNDS.DONE); - setDidConfirm(true); - onConfirm?.(selectedParticipants); - } - }, - [ - selectedParticipants, - isMerchantRequired, - isMerchantEmpty, - shouldDisplayFieldError, - transaction, - iouType, - onSendMoney, - iouCurrencyCode, - isDistanceRequest, - isDistanceRequestWithPendingRoute, - iouAmount, - isEditingSplitBill, - formError, - onConfirm, - ], - ); - - const footerContent = useMemo(() => { - if (isReadOnly) { - return; - } - - const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; - - const button = shouldShowSettlementButton ? ( - - ) : ( - confirm(value as PaymentMethodType)} - options={splitOrRequestOptions} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} - enterKeyEventListenerPriority={1} - /> - ); - - return ( - <> - {!!formError && ( - - )} - - {button} - - ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". - // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. - const classifiedFields = [ - { - item: ( - { - if (isDistanceRequest) { - return; - } - if (isEditingSplitBill) { - Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID ?? '', CONST.EDIT_REQUEST_FIELD.AMOUNT)); - return; - } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction ?? null) ? translate('common.error.enterAmount') : ''} - /> - ), - shouldShow: shouldShowSmartScanFields, - isSupplementary: false, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - ), - shouldShow: true, - isSupplementary: false, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - disabled={didConfirm} - // todo: handle edit for transaction while moving from track expense - interactive={!isReadOnly && !isMovingTransactionFromTrackExpense} - /> - ), - shouldShow: isDistanceRequest, - isSupplementary: true, - }, - { - item: ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={merchantError ? translate('common.error.fieldRequired') : ''} - rightLabel={isMerchantRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowMerchant, - isSupplementary: !isMerchantRequired, - }, - { - item: ( - { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} - /> - ), - shouldShow: shouldShowDate, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isCategoryRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowCategories, - isSupplementary: action === CONST.IOU.ACTION.CATEGORIZE ? false : !isCategoryRequired, - }, - ...policyTagLists.map(({name, required}, index) => { - const isTagRequired = required === undefined ? false : canUseViolations && required; - return { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(action, iouType, index, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - style={[styles.moneyRequestMenuItem]} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={isTagRequired ? translate('common.required') : ''} - /> - ), - shouldShow: shouldShowTags, - isSupplementary: !isTagRequired, - }; - }), - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - ), - shouldShow: shouldShowTax, - isSupplementary: true, - }, - { - item: ( - - {translate('common.billable')} - onToggleBillable?.(isOn)} - /> - - ), - shouldShow: shouldShowBillable, - isSupplementary: true, - }, - ]; - - const primaryFields = classifiedFields.filter((classifiedField) => classifiedField.shouldShow && !classifiedField.isSupplementary).map((primaryField) => primaryField.item); - - const supplementaryFields = classifiedFields - .filter((classifiedField) => classifiedField.shouldShow && classifiedField.isSupplementary) - .map((supplementaryField) => supplementaryField.item); - - const { - image: receiptImage, - thumbnail: receiptThumbnail, - isThumbnail, - fileExtension, - isLocalFile, - } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : ({} as ReceiptUtils.ThumbnailAndImageURI); - - const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? ''); - const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? ''); - - const receiptThumbnailContent = useMemo( - () => - isLocalFile && Str.isPDF(receiptFilename) ? ( - setIsAttachmentInvalid(true)} - /> - ) : ( - - ), - [isLocalFile, receiptFilename, resolvedThumbnail, styles.moneyRequestImage, isAttachmentInvalid, isThumbnail, resolvedReceiptImage, receiptThumbnail, fileExtension], - ); - - return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - - {isDistanceRequest && ( - - - - )} - {(!isMovingTransactionFromTrackExpense || !hasRoute) && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (receiptImage || receiptThumbnail - ? receiptThumbnailContent - : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( - CONST.IOU.ACTION.CREATE, - iouType, - transaction?.transactionID ?? '', - reportID, - Navigation.getActiveRouteWithoutParams(), - ), - ) - } - /> - ))} - {primaryFields} - {!shouldShowAllFields && ( - - -