diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 24bb3b72a407..e260086e8480 100644 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1,15 +1,11 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports -import {InteractionManager, Keyboard, View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDebouncedState from '@hooks/useDebouncedState'; import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode'; -import useLocalize from '@hooks/useLocalize'; import {MouseProvider} from '@hooks/useMouseContext'; -import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; @@ -17,76 +13,52 @@ import usePreferredPolicy from '@hooks/usePreferredPolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; -import {computePerDiemExpenseAmount, isValidPerDiemExpenseAmount} from '@libs/actions/IOU/PerDiem'; -import {resetSplitShares, setIndividualShare} from '@libs/actions/IOU/Split'; -import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; import {isCategoryDescriptionRequired} from '@libs/CategoryUtils'; -import {convertToBackendAmount, convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import {calculateAmount, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils'; -import Log from '@libs/Log'; -import {validateAmount} from '@libs/MoneyRequestUtils'; +import {isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getIOUConfirmationOptionsFromPayeePersonalDetail, hasEnabledOptions} from '@libs/OptionsListUtils'; -import {getTagLists, isAttendeeTrackingEnabled, isTaxTrackingEnabled} from '@libs/PolicyUtils'; +import {hasEnabledOptions} from '@libs/OptionsListUtils'; +import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; -import {hasEnabledTags, hasMatchingTag} from '@libs/TagsOptionsListUtils'; -import {isValidTimeExpenseAmount} from '@libs/TimeTrackingUtils'; import { - areRequiredFieldsEmpty, - calculateTaxAmount, getAttendees, getCategory, getCurrency, - getDefaultTaxCode, - getDistanceInMeters, getMerchant, getRateID, - getTag, - getTaxValue, - hasMissingSmartscanFields, - hasRoute as hasRouteUtil, - hasTaxRateWithMatchingValue, hasValidModifiedAmount, isDistanceRequest as isDistanceRequestUtil, isGPSDistanceRequest as isGPSDistanceRequestUtil, isManualDistanceRequest as isManualDistanceRequestUtil, - isMerchantMissing, - isScanning, - isScanRequest as isScanRequestUtil, } from '@libs/TransactionUtils'; -import {isInvalidMerchantValue, isValidInputLength} from '@libs/ValidationUtils'; -import {getIsViolationFixed} from '@libs/Violations/ViolationsUtils'; -import {hasInvoicingDetails} from '@userActions/Policy/Policy'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; 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 Button from './Button'; -import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from './DelegateNoAccessModalProvider'; -import FormHelpMessage from './FormHelpMessage'; -import MoneyRequestAmountInput from './MoneyRequestAmountInput'; +import buildConfirmAction from './MoneyRequestConfirmationList/confirmAction'; +import ConfirmationFooterContent from './MoneyRequestConfirmationList/ConfirmationFooterContent'; import ConfirmationTelemetry from './MoneyRequestConfirmationList/ConfirmationTelemetry'; import DistanceRequestController from './MoneyRequestConfirmationList/DistanceRequestController'; import FieldAutoSelector from './MoneyRequestConfirmationList/FieldAutoSelector'; +import useConfirmationAmount from './MoneyRequestConfirmationList/hooks/useConfirmationAmount'; +import useConfirmationCtaText from './MoneyRequestConfirmationList/hooks/useConfirmationCtaText'; +import useConfirmationSections from './MoneyRequestConfirmationList/hooks/useConfirmationSections'; +import useConfirmationValidation from './MoneyRequestConfirmationList/hooks/useConfirmationValidation'; +import useDistanceRequestState from './MoneyRequestConfirmationList/hooks/useDistanceRequestState'; +import useFormErrorManagement from './MoneyRequestConfirmationList/hooks/useFormErrorManagement'; +import usePolicyCategoriesForConfirmation from './MoneyRequestConfirmationList/hooks/usePolicyCategoriesForConfirmation'; +import usePolicyTagsForConfirmation from './MoneyRequestConfirmationList/hooks/usePolicyTagsForConfirmation'; +import useReceiptTraining from './MoneyRequestConfirmationList/hooks/useReceiptTraining'; +import useSplitParticipants from './MoneyRequestConfirmationList/hooks/useSplitParticipants'; +import useTaxAmount from './MoneyRequestConfirmationList/hooks/useTaxAmount'; +import useTransactionReportForConfirmation from './MoneyRequestConfirmationList/hooks/useTransactionReportForConfirmation'; import SplitBillController from './MoneyRequestConfirmationList/SplitBillController'; import TaxController from './MoneyRequestConfirmationList/TaxController'; import MoneyRequestConfirmationListFooter from './MoneyRequestConfirmationListFooter'; -import {PressableWithFeedback} from './Pressable'; -import {useProductTrainingContext} from './ProductTrainingContext'; import UserListItem from './SelectionList/ListItem/UserListItem'; import SelectionListWithSections from './SelectionList/SelectionListWithSections'; -import type {Section} from './SelectionList/SelectionListWithSections/types'; -import SettlementButton from './SettlementButton'; -import type {PaymentActionParams} from './SettlementButton/types'; -import Text from './Text'; -import EducationalTooltip from './Tooltip/EducationalTooltip'; type MoneyRequestConfirmationListProps = { /** Callback to inform parent modal of success */ @@ -188,10 +160,6 @@ type MoneyRequestConfirmationListProps = { type MoneyRequestConfirmationListItem = (Participant & {keyForList: string}) | OptionData; -const mileageRateSelector = (policy: OnyxEntry) => DistanceRequestUtils.getDefaultMileageRate(policy); -const transactionReportSelector = (report: OnyxEntry) => report && ({type: report.type} as OnyxEntry); -const policyDraftSelector = (draft: OnyxEntry) => draft && ({customUnits: draft.customUnits} as OnyxEntry); - function MoneyRequestConfirmationList({ transaction, onSendMoney, @@ -226,39 +194,20 @@ function MoneyRequestConfirmationList({ isTimeRequest = false, shouldHideToSection = false, }: MoneyRequestConfirmationListProps) { - const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); - const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, { - selector: transactionReportSelector, - }); - const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, { - selector: policyDraftSelector, - }); - const [defaultMileageRateDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, { - selector: mileageRateSelector, - }); + const policyCategories = usePolicyCategoriesForConfirmation(policyID); + const {policyTags, policyTagLists} = usePolicyTagsForConfirmation(policyID); + const transactionReport = useTransactionReportForConfirmation(transaction?.reportID); const {policyForMovingExpenses, shouldSelectPolicy} = usePolicyForMovingExpenses(); const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseUtil(action); - const [defaultMileageRateReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - selector: mileageRateSelector, - }); - const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`); - const {convertToDisplayString, getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions(); const {isBetaEnabled} = usePermissions(); const isNewManualExpenseFlowEnabled = isBetaEnabled(CONST.BETAS.NEW_MANUAL_EXPENSE_FLOW); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const isInLandscapeMode = useIsInLandscapeMode(); - const isTestReceipt = useMemo(() => { - return transaction?.receipt?.isTestReceipt ?? false; - }, [transaction?.receipt?.isTestReceipt]); - - const isTestDriveReceipt = useMemo(() => { - return transaction?.receipt?.isTestDriveReceipt ?? false; - }, [transaction?.receipt?.isTestDriveReceipt]); - - const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_DRIVE_CONFIRMATION, isTestDriveReceipt); + const {isTestReceipt, shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useReceiptTraining({ + transaction, + }); const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK; const {policy} = usePolicyForTransaction({ @@ -269,11 +218,7 @@ function MoneyRequestConfirmationList({ isPerDiemRequest, }); - const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; - const defaultMileageRate = defaultMileageRateDraft ?? defaultMileageRateReal; - const styles = useThemeStyles(); - const {translate} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {isRestrictedToPreferredPolicy} = usePreferredPolicy(); @@ -285,14 +230,12 @@ function MoneyRequestConfirmationList({ const iouCurrencyCode = getCurrency(transaction); const iouMerchant = getMerchant(transaction); const iouCategory = getCategory(transaction); - const iouAttendees = useMemo(() => getAttendees(transaction, currentUserPersonalDetails), [transaction, currentUserPersonalDetails]); + const iouAttendees = getAttendees(transaction, currentUserPersonalDetails); const isTypeRequest = iouType === CONST.IOU.TYPE.SUBMIT; - const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.PAY; const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK; const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; - const isScanRequest = useMemo(() => isScanRequestUtil(transaction), [transaction]); const isFromGlobalCreateAndCanEditParticipant = !!transaction?.isFromGlobalCreate && !isPerDiemRequest && !isTimeRequest; const transactionID = transaction?.transactionID; @@ -301,24 +244,18 @@ function MoneyRequestConfirmationList({ const subRates = transaction?.comment?.customUnit?.subRates ?? []; const prevSubRates = usePrevious(subRates); - const defaultRate = defaultMileageRate?.customUnitRateID; - const mileageRate = DistanceRequestUtils.getRate({ - transaction, - policy, - ...(isMovingTransactionFromTrackExpense && {policyForMovingExpenses}), - isMovingTransactionFromTrackExpense, - policyDraft, - }); - const distanceRate = mileageRate.rate; - const distanceUnit = mileageRate.unit; - const calculateFromTransactionData = isMovingTransactionFromTrackExpense && !distanceRate; - const unit = calculateFromTransactionData ? transaction?.comment?.customUnit?.distanceUnit : distanceUnit; - const rate = calculateFromTransactionData ? Math.abs(iouAmount) / (transaction?.comment?.customUnit?.quantity ?? 1) : distanceRate; - const currency = calculateFromTransactionData ? iouCurrencyCode : (mileageRate.currency ?? CONST.CURRENCY.USD); - const prevRate = usePrevious(rate); - const prevUnit = usePrevious(unit); - const prevCurrency = usePrevious(currency); + const {defaultRate, mileageRate, unit, rate, currency, prevCurrency, distance, shouldCalculateDistanceAmount, hasRoute, isDistanceRequestWithPendingRoute, distanceRequestAmount} = + useDistanceRequestState({ + transaction, + policy, + policyID, + policyForMovingExpenses, + isMovingTransactionFromTrackExpense, + isDistanceRequest, + iouAmount, + iouCurrencyCode, + }); // A flag for showing the categories field const shouldShowCategories = isTrackExpense @@ -327,48 +264,36 @@ function MoneyRequestConfirmationList({ const shouldShowMerchant = (shouldShowSmartScanFields || isTypeSend) && !isDistanceRequest && !isPerDiemRequest && (!isTimeRequest || action !== CONST.IOU.ACTION.CREATE); - const policyTagLists = useMemo(() => getTagLists(policyTags), [policyTags]); - const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat || isTrackExpense, policy, isDistanceRequest, isPerDiemRequest, isTimeRequest); - // Update the tax code when the default changes (for example, because the transaction currency changed) - const defaultTaxCode = getDefaultTaxCode(policy, transaction) ?? (isMovingTransactionFromTrackExpense ? (getDefaultTaxCode(policyForMovingExpenses, transaction) ?? '') : ''); - const defaultTaxValue = getTaxValue(policy, transaction, defaultTaxCode) ?? null; - const previousDefaultTaxCode = getDefaultTaxCode(policy, transaction, previousTransactionCurrency); - const shouldKeepCurrentTaxSelection = hasTaxRateWithMatchingValue(policy, transaction) && transaction?.taxCode !== previousDefaultTaxCode; - - const distance = getDistanceInMeters(transaction, unit); - const prevDistance = usePrevious(distance); - const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency || prevUnit !== unit); - - const shouldCalculatePerDiemAmount = isPerDiemRequest && (iouAmount === 0 || JSON.stringify(prevSubRates) !== JSON.stringify(subRates) || prevCurrency !== currency); - - const hasRoute = hasRouteUtil(transaction, isDistanceRequest); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense; - - const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); - - let amountToBeUsed = iouAmount; + const {defaultTaxCode, defaultTaxValue, shouldKeepCurrentTaxSelection, taxAmountInSmallestCurrencyUnits} = useTaxAmount({ + transaction, + policy, + policyForMovingExpenses, + isDistanceRequest, + isMovingTransactionFromTrackExpense, + customUnitRateID, + distance, + previousTransactionCurrency, + }); - if (shouldCalculateDistanceAmount) { - amountToBeUsed = distanceRequestAmount; - } else if (shouldCalculatePerDiemAmount) { - const perDiemRequestAmount = computePerDiemExpenseAmount({subRates}); - amountToBeUsed = perDiemRequestAmount; - } + const {amountToBeUsed, formattedAmount, formattedAmountPerAttendee, isScanRequest} = useConfirmationAmount({ + transaction, + iouAmount, + iouCurrencyCode, + iouAttendees, + isDistanceRequest, + isDistanceRequestWithPendingRoute, + shouldCalculateDistanceAmount, + distanceRequestAmount, + distanceCurrency: currency, + isPerDiemRequest, + prevCurrency, + currency, + prevSubRates, + }); - let formattedAmount = convertToDisplayString(amountToBeUsed, isDistanceRequest ? currency : iouCurrencyCode); - if (isDistanceRequestWithPendingRoute) { - formattedAmount = ''; - } else if (isScanning(transaction)) { - formattedAmount = translate('iou.receiptStatusTitle'); - } - const formattedAmountPerAttendee = - isDistanceRequestWithPendingRoute || isScanRequest - ? '' - : convertToDisplayString(amountToBeUsed / (iouAttendees?.length && iouAttendees.length > 0 ? iouAttendees.length : 1), isDistanceRequest ? currency : iouCurrencyCode); const isFocused = useIsFocused(); - const [formError, debouncedFormError, setFormError] = useDebouncedState(''); const [didConfirm, setDidConfirm] = useState(isConfirmed); const [didConfirmSplit, setDidConfirmSplit] = useState(false); @@ -378,106 +303,35 @@ function MoneyRequestConfirmationList({ setShowMoreFields(false); }, [transactionID]); - // Clear the form error if it's set to one among the list passed as an argument - const clearFormErrors = useCallback( - (errors: string[]) => { - if (!errors.includes(formError)) { - return; - } - - setFormError(''); - }, - [formError, setFormError], - ); - - const shouldDisplayFieldError: boolean = useMemo(() => { - if (!isEditingSplitBill) { - return false; - } - - return (!!hasSmartScanFailed && hasMissingSmartscanFields(transaction, transactionReport)) || (didConfirmSplit && areRequiredFieldsEmpty(transaction, transactionReport)); - }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit, transactionReport]); - - const isMerchantEmpty = useMemo(() => !iouMerchant || isMerchantMissing(transaction), [transaction, iouMerchant]); - const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant; - const isMerchantFieldValid = useMemo(() => { - const merchantValue = iouMerchant ?? ''; - const trimmedMerchant = merchantValue.trim(); - const {isValid} = isValidInputLength(merchantValue, CONST.MERCHANT_NAME_MAX_BYTES); - - if (!isValid) { - return false; - } - - if (!trimmedMerchant) { - return !isMerchantRequired; - } - - return !isInvalidMerchantValue(trimmedMerchant); - }, [iouMerchant, isMerchantRequired]); - - const isCategoryRequired = !!policy?.requiresCategory && !isTypeInvoice; - - const isDescriptionRequired = useMemo( - () => isCategoryDescriptionRequired(policyCategories, iouCategory, policy?.areRulesEnabled), - [iouCategory, policyCategories, policy?.areRulesEnabled], - ); + const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0); + const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; + const shouldShowReadOnlySplits = isPolicyExpenseChat || isReadOnly || isScanRequest; - const isViolationFixed = getIsViolationFixed(formError, { - category: iouCategory, - tag: getTag(transaction), - taxCode: transaction?.taxCode, - taxValue: transaction?.taxValue, - policyCategories, - policyTagLists: policyTags, - policyTaxRates: policy?.taxRates?.taxes, + const {formError, setFormError, clearFormErrors, shouldDisplayFieldError, isMerchantEmpty, isMerchantRequired, errorMessage} = useFormErrorManagement({ + transaction, + transactionReport, + iouMerchant, + iouCategory, iouAttendees, + policy, + policyTags, + policyCategories, currentUserPersonalDetails, - isAttendeeTrackingEnabled: isAttendeeTrackingEnabled(policy), - isControlPolicy: policy?.type === CONST.POLICY.TYPE.CORPORATE, + isEditingSplitBill, + isPolicyExpenseChat, + isScanRequest, + shouldShowMerchant, + hasSmartScanFailed, + didConfirmSplit, + routeError, + isTypeSplit, + shouldShowReadOnlySplits, }); - useEffect(() => { - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - if (formError === 'iou.error.invalidMerchant' && isMerchantFieldValid) { - setFormError(''); - return; - } - // Check 1: If formError does NOT start with "violations.", clear it and return - // Reset the form error whenever the screen gains or loses focus - // but preserve violation-related errors since those represent real validation issues - // that can only be resolved by fixing the underlying issue - if (formError && !formError.startsWith(CONST.VIOLATIONS_PREFIX)) { - setFormError(''); - return; - } - // Check 2: Only reached if formError STARTS with "violations." - // Clear any violation error if the user has fixed the underlying issue - if (isViolationFixed) { - setFormError(''); - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes - }, [isFocused, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isViolationFixed, isMerchantFieldValid]); - - const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0); + const isCategoryRequired = !!policy?.requiresCategory && !isTypeInvoice; - // Calculate and set tax amount in transaction draft - const taxableAmount = isDistanceRequest ? DistanceRequestUtils.getTaxableAmount(policy, customUnitRateID, distance) : Math.abs(transaction?.amount ?? 0); - // First we'll try to get the tax value from the chosen policy and if not found, we'll try to get it from the policy for moving expenses (only if the transaction is moving from track expense) - const taxPercentage = - getTaxValue(policy, transaction, transaction?.taxCode ?? defaultTaxCode) ?? - (isMovingTransactionFromTrackExpense ? getTaxValue(policyForMovingExpenses, transaction, transaction?.taxCode ?? defaultTaxCode) : ''); - const taxDecimals = getCurrencyDecimals(transaction?.currency ?? CONST.CURRENCY.USD); - const taxAmount = isMovingTransactionFromTrackExpense && transaction?.taxAmount ? Math.abs(transaction?.taxAmount ?? 0) : calculateTaxAmount(taxPercentage, taxableAmount, taxDecimals); + const isDescriptionRequired = isCategoryDescriptionRequired(policyCategories, iouCategory, policy?.areRulesEnabled); - const taxAmountInSmallestCurrencyUnits = convertToBackendAmount(Number.parseFloat(taxAmount.toString())); // If completing a split expense fails, set didConfirm to false to allow the user to edit the fields again if (isEditingSplitBill && didConfirm) { setDidConfirm(false); @@ -487,247 +341,46 @@ function MoneyRequestConfirmationList({ setDidConfirm(isConfirmed); }, [isConfirmed]); - const splitOrRequestOptions: Array> = useMemo(() => { - let text; - if (expensesNumber > 1) { - text = translate('iou.createExpenses', expensesNumber); - } else if (isTypeInvoice) { - if (hasInvoicingDetails(policy)) { - text = translate('iou.sendInvoice', formattedAmount); - } else { - text = translate('common.next'); - } - } else if (isTypeTrackExpense) { - text = translate('iou.createExpense'); - if (iouAmount !== 0 && !isNewManualExpenseFlowEnabled) { - text = translate('iou.createExpenseWithAmount', {amount: formattedAmount}); - } - } else if (isTypeSplit && iouAmount === 0) { - text = translate('iou.splitExpense'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute || isPerDiemRequest) { - text = translate('iou.createExpense'); - if (iouAmount !== 0 && !isNewManualExpenseFlowEnabled) { - text = translate('iou.createExpenseWithAmount', {amount: formattedAmount}); - } - } else if (isTypeSplit) { - text = translate('iou.splitAmount', formattedAmount); - if (isNewManualExpenseFlowEnabled) { - text = translate('iou.splitExpense'); - } - } else if (iouAmount === 0) { - text = translate('iou.createExpense'); - } else if (isNewManualExpenseFlowEnabled) { - text = translate('iou.createExpense'); - } else { - text = translate('iou.createExpenseWithAmount', {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: iouType, - }, - ]; - }, [ + const splitOrRequestOptions = useConfirmationCtaText({ + expensesNumber, isTypeInvoice, isTypeTrackExpense, isTypeSplit, - expensesNumber, - iouAmount, - receiptPath, isTypeRequest, - isDistanceRequestWithPendingRoute, - isPerDiemRequest, + iouAmount, iouType, policy, - translate, formattedAmount, + receiptPath, + isDistanceRequestWithPendingRoute, + isPerDiemRequest, isNewManualExpenseFlowEnabled, - ]); - - const onSplitShareChange = useCallback( - (accountID: number, value: number) => { - if (!transaction?.transactionID) { - return; - } - const amountInCents = convertToBackendAmount(value); - setIndividualShare(transaction?.transactionID, accountID, amountInCents); - }, - [transaction?.transactionID], - ); - - const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); - const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); - const shouldShowReadOnlySplits = useMemo(() => isPolicyExpenseChat || isReadOnly || isScanRequest, [isPolicyExpenseChat, isReadOnly, isScanRequest]); - - const splitParticipants = useMemo(() => { - if (!isTypeSplit) { - return []; - } + }); - const payeeOption = getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails); - if (shouldShowReadOnlySplits) { - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { - const isPayer = participantOption.accountID === payeeOption.accountID; - let amount: number | undefined = 0; - if (iouAmount > 0) { - amount = - transaction?.comment?.splits?.find((split) => split.accountID === participantOption.accountID)?.amount ?? - calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', isPayer); - } - return { - ...participantOption, - keyForList: `${participantOption.keyForList ?? participantOption.accountID ?? participantOption.reportID}`, - isSelected: false, - isInteractive: false, - rightElement: ( - - {amount ? convertToDisplayString(amount, iouCurrencyCode) : ''} - - ), - }; - }); - } + const selectedParticipants = selectedParticipantsProp.filter((participant) => participant.selected); + const payeePersonalDetails = payeePersonalDetailsProp ?? currentUserPersonalDetails; - const currencySymbol = getCurrencySymbol(iouCurrencyCode ?? '') ?? iouCurrencyCode; - const formattedTotalAmount = convertToDisplayStringWithoutCurrency(iouAmount, iouCurrencyCode); - - return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => ({ - ...participantOption, - tabIndex: -1, - isSelected: false, - isInteractive: false, - keyForList: `${participantOption.keyForList ?? participantOption.accountID ?? participantOption.reportID}`, - rightElement: ( - onSplitShareChange(participantOption.accountID ?? CONST.DEFAULT_NUMBER_ID, Number(value))} - maxLength={formattedTotalAmount.length + 1} - contentWidth={(formattedTotalAmount.length + 1) * CONST.CHARACTER_WIDTH} - shouldApplyPaddingToContainer - shouldUseDefaultLineHeightForPrefix={false} - shouldWrapInputInContainer={false} - /> - ), - })); - }, [ + const {splitParticipants, getSplitSectionHeader} = useSplitParticipants({ isTypeSplit, - payeePersonalDetails, shouldShowReadOnlySplits, - iouCurrencyCode, - iouAmount, + payeePersonalDetails, selectedParticipants, - styles.flexWrap, - styles.pl2, - styles.pr1, - styles.h100, - styles.textLabel, - styles.pv0, - styles.lineHeightUndefined, - styles.optionRowAmountInput, - styles.textInputContainer, - styles.ml3, - transaction?.comment?.splits, - transaction?.splitShares, - onSplitShareChange, - getCurrencySymbol, - convertToDisplayString, - ]); - - const isSplitModified = useMemo(() => { - if (!transaction?.splitShares) { - return; - } - return Object.keys(transaction.splitShares).some((key) => transaction.splitShares?.[Number(key) ?? -1]?.isModified); - }, [transaction?.splitShares]); - - const getSplitSectionHeader = useCallback( - () => ( - - {translate('iou.participants')} - {!shouldShowReadOnlySplits && !!isSplitModified && ( - { - // Dismiss the keyboard so that MoneyRequestAmountInput's useEffect syncs the new amount. - // Without this, the effect skips the update while the input is focused (see formatAmountOnBlur guard). - Keyboard.dismiss(); - resetSplitShares(transaction); - }} - accessibilityLabel={CONST.ROLE.BUTTON} - role={CONST.ROLE.BUTTON} - shouldUseAutoHitSlop - sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.RESET_SPLIT_SHARES} - > - {translate('common.reset')} - - )} - - ), - [ - isSplitModified, - shouldShowReadOnlySplits, - styles.flexRow, - styles.justifyContentBetween, - styles.link, - styles.mb1, - styles.mt2, - styles.ph5, - styles.pr5, - styles.textLabelSupporting, - transaction, - translate, - ], - ); + transaction, + iouAmount, + iouCurrencyCode, + }); const canEditParticipant = isFromGlobalCreateAndCanEditParticipant && !isTestReceipt && (!isRestrictedToPreferredPolicy || isTypeInvoice); - const sections = useMemo(() => { - const options: Array> = []; - if (isTypeSplit) { - options.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)], - sectionIndex: 0, - }, - { - customHeader: getSplitSectionHeader(), - data: splitParticipants, - sectionIndex: 1, - }, - ); - // When adding an expense from within a report, hide the "To:" section since the destination is already the current report - } else if (!shouldHideToSection) { - const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, - isSelected: false, - keyForList: `${participant.keyForList ?? participant.accountID ?? participant.reportID}`, - isInteractive: canEditParticipant, - shouldShowRightCaret: canEditParticipant, - })); - - options.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - sectionIndex: 0, - }); - } - - return options; - }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants, canEditParticipant, shouldHideToSection]); + const sections = useConfirmationSections({ + isTypeSplit, + shouldHideToSection, + canEditParticipant, + payeePersonalDetails, + splitParticipants, + selectedParticipants, + getSplitSectionHeader, + }); /** * Navigate to the participant step @@ -741,185 +394,51 @@ function MoneyRequestConfirmationList({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(newIOUType, transactionID, transaction.reportID, Navigation.getActiveRoute(), action)); }; - /** - * @param {String} paymentMethod - */ - const confirm = useCallback( - ({paymentType: paymentMethod}: PaymentActionParams) => { - if (!!routeError || !transactionID) { - return; - } - if (iouType === CONST.IOU.TYPE.INVOICE && !hasInvoicingDetails(policy)) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_COMPANY_INFO.getRoute(iouType, transactionID, reportID, Navigation.getActiveRoute())); - return; - } - - if (selectedParticipants.length === 0) { - setFormError('iou.error.noParticipantSelected'); - return; - } - - const amountForValidation = iouAmount; - const isAmountMissingForManualFlow = amountForValidation === null || amountForValidation === undefined; - - if (iouType !== CONST.IOU.TYPE.PAY && isNewManualExpenseFlowEnabled && isAmountMissingForManualFlow) { - setFormError('common.error.invalidAmount'); - return; - } - - const merchantValue = iouMerchant ?? ''; - const {isValid: isMerchantLengthValid} = isValidInputLength(merchantValue, CONST.MERCHANT_NAME_MAX_BYTES); - - if (!isMerchantLengthValid) { - setFormError('iou.error.invalidMerchant'); - return; - } - - if (!isEditingSplitBill && isMerchantRequired && (isMerchantEmpty || (shouldDisplayFieldError && isMerchantMissing(transaction)))) { - setFormError('iou.error.invalidMerchant'); - return; - } - - if (iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { - setFormError('iou.error.invalidCategoryLength'); - return; - } - - if (iouCategory && policyCategories && !policyCategories[iouCategory]?.enabled) { - setFormError('violations.categoryOutOfPolicy'); - return; - } - - const transactionTag = getTag(transaction); - if (transactionTag.length > CONST.API_TRANSACTION_TAG_MAX_LENGTH) { - setFormError('iou.error.invalidTagLength'); - return; - } - - if (transactionTag && hasEnabledTags(policyTagLists) && !hasMatchingTag(policyTags, transactionTag)) { - setFormError('violations.tagOutOfPolicy'); - return; - } - - // Since invoices are not expense reports that need attendee tracking, this validation should not apply to invoices - const isMissingAttendeesViolation = - iouType !== CONST.IOU.TYPE.INVOICE && - getIsMissingAttendeesViolation( - policyCategories, - iouCategory, - iouAttendees, - currentUserPersonalDetails, - isAttendeeTrackingEnabled(policy), - policy?.type === CONST.POLICY.TYPE.CORPORATE, - ); - if (isMissingAttendeesViolation) { - setFormError('violations.missingAttendees'); - return; - } - - if (shouldShowTax && !!transaction.taxCode && !hasTaxRateWithMatchingValue(policy, transaction)) { - setFormError('violations.taxOutOfPolicy'); - return; - } - - if (isPerDiemRequest && (transaction.comment?.customUnit?.subRates ?? []).length === 0) { - setFormError('iou.error.invalidSubrateLength'); - return; - } - - if (iouType !== CONST.IOU.TYPE.PAY) { - // validate the amount for distance expenses - const decimals = getCurrencyDecimals(iouCurrencyCode); - if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !validateAmount(String(iouAmount), decimals, CONST.IOU.DISTANCE_REQUEST_AMOUNT_MAX_LENGTH)) { - setFormError('common.error.invalidAmount'); - return; - } - - if (isDistanceRequest && Math.abs(iouAmount) > CONST.IOU.MAX_SAFE_AMOUNT) { - setFormError('iou.error.distanceAmountTooLarge'); - return; - } - - if (isTimeRequest && !isValidTimeExpenseAmount(iouAmount, decimals)) { - setFormError('iou.timeTracking.amountTooLargeError'); - return; - } - - if (isPerDiemRequest) { - if (!isValidPerDiemExpenseAmount(transaction.comment?.customUnit ?? {}, decimals)) { - setFormError('iou.error.invalidQuantity'); - return; - } - } - - if (isEditingSplitBill && areRequiredFieldsEmpty(transaction, transactionReport)) { - setDidConfirmSplit(true); - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - - if (isEditingSplitBill && iouAmount === 0) { - setFormError('iou.error.invalidAmount'); - return; - } - - if (formError) { - return; - } - - onConfirm?.(selectedParticipants); - } else { - if (!paymentMethod) { - return; - } - if (isDelegateAccessRestricted) { - showDelegateNoAccessModal(); - return; - } - if (formError) { - return; - } - Log.info(`[IOU] Sending money via: ${paymentMethod}`); - onSendMoney?.(paymentMethod); - } - }, - [ - routeError, - transactionID, - iouType, - policy, - policyTagLists, - selectedParticipants, - isEditingSplitBill, - isMerchantRequired, - isMerchantEmpty, - iouMerchant, - shouldDisplayFieldError, - shouldShowTax, - transaction, - policyTags, - isPerDiemRequest, - reportID, - setFormError, - iouCurrencyCode, - isDistanceRequest, - isDistanceRequestWithPendingRoute, - iouAmount, - formError, - onConfirm, - isDelegateAccessRestricted, - onSendMoney, - showDelegateNoAccessModal, - iouCategory, - policyCategories, - transactionReport, - iouAttendees, - currentUserPersonalDetails, - isTimeRequest, - getCurrencyDecimals, - isNewManualExpenseFlowEnabled, - ], - ); + const {validate} = useConfirmationValidation({ + transaction, + transactionReport, + transactionID, + iouType, + iouAmount, + iouMerchant, + iouCategory, + iouCurrencyCode, + iouAttendees, + policy, + policyTags, + policyTagLists, + policyCategories, + selectedParticipants, + currentUserPersonalDetails, + isEditingSplitBill, + isMerchantRequired, + isMerchantEmpty, + shouldDisplayFieldError, + shouldShowTax, + isDistanceRequest, + isDistanceRequestWithPendingRoute, + isPerDiemRequest, + isTimeRequest, + isNewManualExpenseFlowEnabled, + routeError, + }); + + const confirm = buildConfirmAction({ + iouType, + policy, + transactionID, + reportID, + routeError, + formError, + selectedParticipants, + isDelegateAccessRestricted, + validate, + setFormError, + setDidConfirmSplit, + showDelegateNoAccessModal, + onConfirm, + onSendMoney, + }); const focusTimeoutRef = useRef(null); useFocusEffect( @@ -934,133 +453,30 @@ function MoneyRequestConfirmationList({ }, []), ); - const errorMessage = useMemo(() => { - if (routeError) { - return routeError; - } - if (isTypeSplit && !shouldShowReadOnlySplits) { - return debouncedFormError && translate(debouncedFormError); - } - // Don't show error at the bottom of the form for missing attendees - if (formError === 'violations.missingAttendees') { - return; - } - return formError && translate(formError); - }, [routeError, isTypeSplit, shouldShowReadOnlySplits, debouncedFormError, formError, translate]); - - const footerContent = useMemo(() => { - if (isReadOnly) { - return; - } - - const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.PAY; + const isCompactMode = !showMoreFields && isScanRequest && !isInLandscapeMode; + const selectionListStyle = { + containerStyle: [styles.flexBasisAuto], + contentContainerStyle: isCompactMode ? [styles.flexGrow1] : undefined, + listFooterContentStyle: isCompactMode ? [styles.flex1, styles.mv3] : [styles.mv3], + }; - const button = shouldShowSettlementButton ? ( - - ) : ( - <> - {expensesNumber > 1 && ( -