From 20018215ab1a2ffba7e2228fc323f4104eaed68b Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Fri, 19 Apr 2024 23:16:50 +0100 Subject: [PATCH] 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 && ( - - -