diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu.tsx index 8aa3a5f0b9f0..9466da601825 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu.tsx @@ -10,30 +10,33 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; -type DropdownOption = { - value: T; +type PaymentType = DeepValueOf; + +type DropdownOption = { + value: PaymentType; text: string; - icon?: IconAsset; + icon: IconAsset; iconWidth?: number; iconHeight?: number; iconDescription?: string; }; -type ButtonWithDropdownMenuProps = { +type ButtonWithDropdownMenuProps = { /** Text to display for the menu header */ menuHeaderText?: string; /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: T) => void; + onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: PaymentType) => void; /** Callback to execute when a dropdown option is selected */ - onOptionSelected?: (option: DropdownOption) => void; + onOptionSelected?: (option: DropdownOption) => void; /** Call the onPress function on main button when Enter key is pressed */ pressOnEnter?: boolean; @@ -52,19 +55,19 @@ type ButtonWithDropdownMenuProps = { /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: Array>; + options: DropdownOption[]; /** The anchor alignment of the popover menu */ anchorAlignment?: AnchorAlignment; /* ref for the button */ - buttonRef?: RefObject; + buttonRef: RefObject; /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; }; -function ButtonWithDropdownMenu({ +function ButtonWithDropdownMenu({ isLoading = false, isDisabled = false, pressOnEnter = false, @@ -80,7 +83,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, -}: ButtonWithDropdownMenuProps) { +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/components/ConfirmedRoute.tsx b/src/components/ConfirmedRoute.tsx index da8c0ed86a84..7f05b45bca30 100644 --- a/src/components/ConfirmedRoute.tsx +++ b/src/components/ConfirmedRoute.tsx @@ -25,13 +25,13 @@ type ConfirmedRoutePropsOnyxProps = { type ConfirmedRouteProps = ConfirmedRoutePropsOnyxProps & { /** Transaction that stores the distance request data */ - transaction: Transaction | undefined; + transaction: Transaction; }; function ConfirmedRoute({mapboxAccessToken, transaction}: ConfirmedRouteProps) { const {isOffline} = useNetwork(); - const {route0: route} = transaction?.routes ?? {}; - const waypoints = transaction?.comment?.waypoints ?? {}; + const {route0: route} = transaction.routes ?? {}; + const waypoints = transaction.comment?.waypoints ?? {}; const coordinates = route?.geometry?.coordinates ?? []; const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js new file mode 100755 index 000000000000..df2781d3ea89 --- /dev/null +++ b/src/components/MoneyRequestConfirmationList.js @@ -0,0 +1,898 @@ +import {useIsFocused} from '@react-navigation/native'; +import {format} from 'date-fns'; +import {isEmpty} from 'lodash'; +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import * as IOUUtils from '@libs/IOUUtils'; +import Log from '@libs/Log'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReceiptUtils from '@libs/ReceiptUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; +import {policyPropTypes} from '@pages/workspace/withPolicy'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; +import categoryPropTypes from './categoryPropTypes'; +import ConfirmedRoute from './ConfirmedRoute'; +import FormHelpMessage from './FormHelpMessage'; +import Image from './Image'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import optionPropTypes from './optionPropTypes'; +import OptionsSelector from './OptionsSelector'; +import ReceiptEmptyState from './ReceiptEmptyState'; +import SettlementButton from './SettlementButton'; +import ShowMoreButton from './ShowMoreButton'; +import Switch from './Switch'; +import tagPropTypes from './tagPropTypes'; +import Text from './Text'; +import transactionPropTypes from './transactionPropTypes'; +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; + +const propTypes = { + /** Callback to inform parent modal of success */ + onConfirm: PropTypes.func, + + /** Callback to parent modal to send money */ + onSendMoney: PropTypes.func, + + /** Callback to inform a participant is selected */ + onSelectParticipant: PropTypes.func, + + /** Should we request a single or multiple participant selection from user */ + hasMultipleParticipants: PropTypes.bool.isRequired, + + /** IOU amount */ + iouAmount: PropTypes.number.isRequired, + + /** IOU comment */ + iouComment: PropTypes.string, + + /** IOU currency */ + iouCurrencyCode: PropTypes.string, + + /** IOU type */ + iouType: PropTypes.string, + + /** IOU date */ + iouCreated: PropTypes.string, + + /** IOU merchant */ + iouMerchant: PropTypes.string, + + /** IOU Category */ + iouCategory: PropTypes.string, + + /** IOU Tag */ + iouTag: PropTypes.string, + + /** IOU isBillable */ + iouIsBillable: PropTypes.bool, + + /** Callback to toggle the billable state */ + onToggleBillable: PropTypes.func, + + /** Selected participants from MoneyRequestModal with login / accountID */ + selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, + + /** Payee of the money request with login */ + payeePersonalDetails: optionPropTypes, + + /** Can the participants be modified or not */ + canModifyParticipants: PropTypes.bool, + + /** Should the list be read only, and not editable? */ + isReadOnly: PropTypes.bool, + + /** Depending on expense report or personal IOU report, respective bank account route */ + bankAccountRoute: PropTypes.string, + + ...withCurrentUserPersonalDetailsPropTypes, + + /** Current user session */ + session: PropTypes.shape({ + email: PropTypes.string.isRequired, + }), + + /** The policyID of the request */ + policyID: PropTypes.string, + + /** The reportID of the request */ + reportID: PropTypes.string, + + /** File path of the receipt */ + receiptPath: PropTypes.string, + + /** File name of the receipt */ + receiptFilename: PropTypes.string, + + /** List styles for OptionsSelector */ + listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + + /** ID of the transaction that represents the money request */ + transactionID: PropTypes.string, + + /** Transaction that represents the money request */ + transaction: transactionPropTypes, + + /** Unit and rate used for if the money request is a distance request */ + mileageRate: PropTypes.shape({ + /** Unit used to represent distance */ + unit: PropTypes.oneOf([CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS]), + + /** Rate used to calculate the distance request amount */ + rate: PropTypes.number, + + /** The currency of the rate */ + currency: PropTypes.string, + }), + + /** Whether the money request is a distance request */ + isDistanceRequest: PropTypes.bool, + + /** Whether the money request is a scan request */ + isScanRequest: PropTypes.bool, + + /** Whether we're editing a split bill */ + isEditingSplitBill: PropTypes.bool, + + /** Whether we should show the amount, date, and merchant fields. */ + shouldShowSmartScanFields: PropTypes.bool, + + /** A flag for verifying that the current report is a sub-report of a workspace chat */ + isPolicyExpenseChat: PropTypes.bool, + + /* Onyx Props */ + /** Collection of categories attached to a policy */ + policyCategories: PropTypes.objectOf(categoryPropTypes), + + /** Collection of tags attached to a policy */ + policyTags: tagPropTypes, + + /* Onyx Props */ + /** The policy of the report */ + policy: policyPropTypes.policy, + + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: iouPropTypes, +}; + +const defaultProps = { + onConfirm: () => {}, + onSendMoney: () => {}, + onSelectParticipant: () => {}, + iouType: CONST.IOU.TYPE.REQUEST, + iouCategory: '', + iouTag: '', + iouIsBillable: false, + onToggleBillable: () => {}, + payeePersonalDetails: null, + canModifyParticipants: false, + isReadOnly: false, + bankAccountRoute: '', + session: { + email: null, + }, + policyID: '', + reportID: '', + ...withCurrentUserPersonalDetailsDefaultProps, + receiptPath: '', + receiptFilename: '', + listStyles: [], + policy: {}, + policyCategories: {}, + policyTags: {}, + transactionID: '', + transaction: {}, + mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, + isDistanceRequest: false, + isScanRequest: false, + shouldShowSmartScanFields: true, + isPolicyExpenseChat: false, + iou: iouDefaultProps, +}; + +function MoneyRequestConfirmationList(props) { + const theme = useTheme(); + const styles = useThemeStyles(); + // Destructure functions from props to pass it as a dependecy to useCallback/useMemo hooks. + // Prop functions pass props itself as a "this" value to the function which means they change every time props change. + const {onSendMoney, onConfirm, onSelectParticipant} = props; + const {translate, toLocaleDigit} = useLocalize(); + const transaction = props.transaction; + const {canUseViolations} = usePermissions(); + + const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; + const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; + const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; + + const isSplitWithScan = isSplitBill && props.isScanRequest; + + const {unit, rate, currency} = props.mileageRate; + const distance = lodashGet(transaction, 'routes.route0.distance', 0); + const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; + const taxRates = lodashGet(props.policy, 'taxRates', {}); + + // A flag for showing the categories field + const shouldShowCategories = props.isPolicyExpenseChat && (props.iouCategory || OptionsListUtils.hasEnabledOptions(_.values(props.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 send money request + const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill; + + // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item + const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; + const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !props.isDistanceRequest && !isSplitWithScan; + + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(props.policyTags), [props.policyTags]); + + // A flag for showing the tags field + const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + + // A flag for showing tax fields - tax rate and tax amount + const shouldShowTax = props.isPolicyExpenseChat && lodashGet(props.policy, 'tax.trackingEnabled', props.policy.isTaxTrackingEnabled); + + // A flag for showing the billable field + const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); + + const hasRoute = TransactionUtils.hasRoute(transaction); + const isDistanceRequestWithPendingRoute = props.isDistanceRequest && (!hasRoute || !rate); + const formattedAmount = isDistanceRequestWithPendingRoute + ? '' + : CurrencyUtils.convertToDisplayString( + shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, + props.isDistanceRequest ? currency : props.iouCurrencyCode, + ); + const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + + const defaultTaxKey = taxRates.defaultExternalID; + const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; + const taxRateTitle = (props.transaction.taxRate && props.transaction.taxRate.text) || defaultTaxName; + + const isFocused = useIsFocused(); + const [formError, setFormError] = useState(''); + + const [didConfirm, setDidConfirm] = useState(false); + const [didConfirmSplit, setDidConfirmSplit] = useState(false); + + const shouldDisplayFieldError = useMemo(() => { + if (!props.isEditingSplitBill) { + return false; + } + + return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); + }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); + + const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; + + useEffect(() => { + if (shouldDisplayFieldError && didConfirmSplit) { + setFormError('iou.error.genericSmartscanFailureMessage'); + return; + } + if (shouldDisplayFieldError && props.hasSmartScanFailed) { + setFormError('iou.receiptScanningFailed'); + return; + } + // reset the form error whenever the screen gains or loses focus + setFormError(''); + }, [isFocused, transaction, shouldDisplayFieldError, props.hasSmartScanFailed, didConfirmSplit]); + + useEffect(() => { + if (!shouldCalculateDistanceAmount) { + return; + } + + const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); + IOU.setMoneyRequestAmount(amount); + }, [shouldCalculateDistanceAmount, distance, rate, unit]); + + /** + * Returns the participants with amount + * @param {Array} participants + * @returns {Array} + */ + const getParticipantsWithAmount = useCallback( + (participantsList) => { + const iouAmount = IOUUtils.calculateAmount(participantsList.length, props.iouAmount, props.iouCurrencyCode); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( + participantsList, + props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(iouAmount, props.iouCurrencyCode) : '', + ); + }, + [props.iouAmount, props.iouCurrencyCode], + ); + + // If completing a split bill fails, set didConfirm to false to allow the user to edit the fields again + if (props.isEditingSplitBill && didConfirm) { + setDidConfirm(false); + } + + const splitOrRequestOptions = useMemo(() => { + let text; + if (isSplitBill && props.iouAmount === 0) { + text = translate('iou.split'); + } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { + text = translate('iou.request'); + if (props.iouAmount !== 0) { + text = translate('iou.requestAmount', {amount: formattedAmount}); + } + } else { + const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; + text = translate(translationKey, {amount: formattedAmount}); + } + return [ + { + text: text[0].toUpperCase() + text.slice(1), + value: props.iouType, + }, + ]; + }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + + const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]); + const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); + const canModifyParticipants = !props.isReadOnly && props.canModifyParticipants && props.hasMultipleParticipants; + const shouldDisablePaidBySection = canModifyParticipants; + + const optionSelectorSections = useMemo(() => { + const sections = []; + const unselectedParticipants = _.filter(props.selectedParticipants, (participant) => !participant.selected); + if (props.hasMultipleParticipants) { + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); + let formattedParticipantsList = _.union(formattedSelectedParticipants, unselectedParticipants); + + if (!canModifyParticipants) { + formattedParticipantsList = _.map(formattedParticipantsList, (participant) => ({ + ...participant, + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + })); + } + + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, props.iouAmount, props.iouCurrencyCode, true); + const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( + payeePersonalDetails, + props.iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, props.iouCurrencyCode) : '', + ); + + sections.push( + { + title: translate('moneyRequestConfirmationList.paidBy'), + data: [formattedPayeeOption], + shouldShow: true, + indexOffset: 0, + isDisabled: shouldDisablePaidBySection, + }, + { + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + indexOffset: 1, + }, + ); + } else { + const formattedSelectedParticipants = _.map(props.selectedParticipants, (participant) => ({ + ...participant, + isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID), + })); + sections.push({ + title: translate('common.to'), + data: formattedSelectedParticipants, + shouldShow: true, + indexOffset: 0, + }); + } + return sections; + }, [ + props.selectedParticipants, + props.hasMultipleParticipants, + props.iouAmount, + props.iouCurrencyCode, + getParticipantsWithAmount, + selectedParticipants, + payeePersonalDetails, + translate, + shouldDisablePaidBySection, + canModifyParticipants, + ]); + + const selectedOptions = useMemo(() => { + if (!props.hasMultipleParticipants) { + return []; + } + return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; + }, [selectedParticipants, props.hasMultipleParticipants, payeePersonalDetails]); + + useEffect(() => { + if (!props.isDistanceRequest) { + 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(props.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); + + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); + IOU.setMoneyRequestMerchant(props.transactionID, distanceMerchant, false); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); + + /** + * @param {Object} option + */ + const selectParticipant = useCallback( + (option) => { + // Return early if selected option is currently logged in user. + if (option.accountID === props.session.accountID) { + return; + } + onSelectParticipant(option); + }, + [props.session.accountID, onSelectParticipant], + ); + + /** + * Navigate to report details or profile of selected user + * @param {Object} option + */ + const navigateToReportOrUserDetail = (option) => { + if (option.accountID) { + const activeRoute = Navigation.getActiveRouteWithoutParams(); + + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); + } else if (option.reportID) { + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); + } + }; + + /** + * @param {String} paymentMethod + */ + const confirm = useCallback( + (paymentMethod) => { + if (_.isEmpty(selectedParticipants)) { + return; + } + if (props.iouCategory && props.iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { + setFormError('iou.error.invalidCategoryLength'); + return; + } + if (props.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 requests + const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode); + if (props.isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { + setFormError('common.error.invalidAmount'); + return; + } + + if (props.isEditingSplitBill && TransactionUtils.areRequiredFieldsEmpty(transaction)) { + setDidConfirmSplit(true); + return; + } + + setDidConfirm(true); + onConfirm(selectedParticipants); + } + }, + [ + selectedParticipants, + onSendMoney, + onConfirm, + props.isEditingSplitBill, + props.iouType, + props.isDistanceRequest, + props.iouCategory, + isDistanceRequestWithPendingRoute, + props.iouCurrencyCode, + props.iouAmount, + transaction, + ], + ); + + const footerContent = useMemo(() => { + if (props.isReadOnly) { + return; + } + + const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; + const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; + + const button = shouldShowSettlementButton ? ( + + ) : ( + confirm(value)} + options={splitOrRequestOptions} + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} + enterKeyEventListenerPriority={1} + /> + ); + + return ( + <> + {!_.isEmpty(formError) && ( + + )} + {button} + + ); + }, [ + props.isReadOnly, + props.iouType, + props.bankAccountRoute, + props.iouCurrencyCode, + props.policyID, + selectedParticipants.length, + shouldDisplayMerchantError, + confirm, + splitOrRequestOptions, + formError, + styles.ph1, + styles.mb2, + ]); + + const {image: receiptImage, thumbnail: receiptThumbnail} = + props.receiptPath && props.receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, props.receiptPath, props.receiptFilename) : {}; + return ( + + {props.isDistanceRequest && ( + + + + )} + {receiptImage || receiptThumbnail ? ( + + ) : ( + // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(props.policy) && + !props.isDistanceRequest && + props.iouType === CONST.IOU.TYPE.REQUEST && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( + CONST.IOU.ACTION.CREATE, + props.iouType, + transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) + } + /> + ) + )} + {props.shouldShowSmartScanFields && ( + { + if (props.isDistanceRequest) { + return; + } + if (props.isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(props.reportID, props.reportActionID, CONST.EDIT_REQUEST_FIELD.AMOUNT)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_AMOUNT.getRoute(props.iouType, props.reportID)); + }} + style={[styles.moneyRequestMenuItem, styles.mt2]} + titleStyle={styles.moneyRequestConfirmationAmount} + disabled={didConfirm} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + error={shouldDisplayFieldError && TransactionUtils.isAmountMissing(transaction) ? translate('common.error.enterAmount') : ''} + /> + )} + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( + CONST.IOU.ACTION.EDIT, + props.iouType, + transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!props.isReadOnly} + numberOfLinesTitle={2} + /> + {!shouldShowAllFields && ( + + )} + {shouldShowAllFields && ( + <> + {shouldShowDate && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( + CONST.IOU.ACTION.EDIT, + props.iouType, + transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + disabled={didConfirm} + interactive={!props.isReadOnly} + brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} + /> + )} + {props.isDistanceRequest && ( + Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} + disabled={didConfirm || !isTypeRequest} + interactive={!props.isReadOnly} + /> + )} + {shouldShowMerchant && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( + CONST.IOU.ACTION.EDIT, + props.iouType, + transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + disabled={didConfirm} + interactive={!props.isReadOnly} + brickRoadIndicator={ + props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '' + } + error={ + shouldDisplayMerchantError || (props.isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction)) + ? translate('common.error.enterMerchant') + : '' + } + /> + )} + {shouldShowCategories && ( + { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( + CONST.IOU.ACTION.EDIT, + props.iouType, + props.transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm} + interactive={!props.isReadOnly} + rightLabel={canUseViolations && Boolean(props.policy.requiresCategory) ? translate('common.required') : ''} + /> + )} + {shouldShowTags && + _.map(policyTagLists, ({name}, index) => ( + { + if (props.isEditingSplitBill) { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.SPLIT, + index, + props.transaction.transactionID, + props.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_TAG.getRoute(props.iouType, props.reportID)); + }} + style={[styles.moneyRequestMenuItem]} + disabled={didConfirm} + interactive={!props.isReadOnly} + rightLabel={canUseViolations && Boolean(props.policy.requiresTag) ? translate('common.required') : ''} + /> + ))} + + {shouldShowTax && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + disabled={didConfirm} + interactive={!props.isReadOnly} + /> + )} + + {shouldShowTax && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(props.iouType, props.transaction.transactionID, props.reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + disabled={didConfirm} + interactive={!props.isReadOnly} + /> + )} + + {shouldShowBillable && ( + + {translate('common.billable')} + + + )} + + )} + + ); +} + +MoneyRequestConfirmationList.propTypes = propTypes; +MoneyRequestConfirmationList.defaultProps = defaultProps; +MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; + +export default compose( + withCurrentUserPersonalDetails, + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + policyCategories: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + }, + policyTags: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, + }, + mileageRate: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: DistanceRequestUtils.getDefaultMileageRate, + }, + splitTransactionDraft: { + key: ({transactionID}) => `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, + }, + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, + iou: { + key: ONYXKEYS.IOU, + }, + }), +)(MoneyRequestConfirmationList); diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx deleted file mode 100755 index 773e98b6462e..000000000000 --- a/src/components/MoneyRequestConfirmationList.tsx +++ /dev/null @@ -1,904 +0,0 @@ -import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; -import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; -import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import * as IOUUtils from '@libs/IOUUtils'; -import Log from '@libs/Log'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; -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 {MileageRate} from '@src/types/onyx/Policy'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import ConfirmedRoute from './ConfirmedRoute'; -import FormHelpMessage from './FormHelpMessage'; -import Image from './Image'; -import MenuItemWithTopDescription from './MenuItemWithTopDescription'; -import OptionsSelector from './OptionsSelector'; -import ReceiptEmptyState from './ReceiptEmptyState'; -import SettlementButton from './SettlementButton'; -import ShowMoreButton from './ShowMoreButton'; -import Switch from './Switch'; -import Text from './Text'; -import type {WithCurrentUserPersonalDetailsProps} from './withCurrentUserPersonalDetails'; -import withCurrentUserPersonalDetails from './withCurrentUserPersonalDetails'; - -type DropdownOption = { - text: string; - value: DeepValueOf; -}; - -type Option = Partial; - -type CategorySection = { - title: string | undefined; - shouldShow: boolean; - indexOffset: number; - data: Option[]; -}; - -type MoneyRequestConfirmationListOnyxProps = { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: OnyxEntry; - - /** Unit and rate used for if the money request is a distance request */ - mileageRate: OnyxEntry; - - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry; - - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry; - - /** The policy of root parent report */ - policy: OnyxEntry; - - /** The session of the logged in user */ - session: OnyxEntry; -}; - -type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & - WithCurrentUserPersonalDetailsProps & { - /** Callback to inform parent modal of success */ - onConfirm?: (selectedParticipants: Participant[]) => void; - - /** Callback to parent modal to send money */ - onSendMoney?: (paymentMethod: PaymentMethodType) => 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?: ValueOf; - - /** IOU date */ - iouCreated?: string; - - /** IOU merchant */ - iouMerchant?: string; - - /** IOU Category */ - iouCategory?: string; - - /** IOU Tag */ - iouTag?: string; - - /** IOU isBillable */ - iouIsBillable?: boolean; - - /** Callback to toggle the billable state */ - onToggleBillable?: () => void; - - /** Selected participants from MoneyRequestModal with login / accountID */ - selectedParticipants: Participant[]; - - /** Payee of the money request with login */ - payeePersonalDetails?: OnyxEntry; - - /** Can the participants be modified or not */ - canModifyParticipants?: boolean; - - /** 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; - - /** ID of the transaction that represents the money request */ - transactionID?: string; - - /** Whether the money request is a distance request */ - isDistanceRequest?: boolean; - - /** Whether the money request is a scan request */ - isScanRequest?: boolean; - - /** Whether we're editing a split bill */ - 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 there is smartscan failed */ - hasSmartScanFailed?: boolean; - - /** ID of the report action */ - reportActionID?: string; - - /** Transaction object */ - transaction?: OnyxTypes.Transaction; - }; - -function MoneyRequestConfirmationList({ - onConfirm = () => {}, - onSendMoney = () => {}, - onSelectParticipant = () => {}, - iouType = CONST.IOU.TYPE.REQUEST, - iouCategory = '', - iouTag = '', - iouIsBillable = false, - onToggleBillable = () => {}, - payeePersonalDetails, - canModifyParticipants = false, - isReadOnly = false, - bankAccountRoute, - policyID, - reportID, - receiptPath, - receiptFilename, - transactionID, - mileageRate, - isDistanceRequest = false, - isScanRequest = false, - shouldShowSmartScanFields = true, - isPolicyExpenseChat = false, - transaction, - iouAmount, - policyTags, - policyCategories, - policy, - iouCurrencyCode, - isEditingSplitBill, - hasSmartScanFailed, - iouMerchant, - currentUserPersonalDetails, - hasMultipleParticipants, - selectedParticipants, - session, - iou, - reportActionID, - iouCreated, - listStyles, - iouComment, -}: MoneyRequestConfirmationListProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); - - const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; - const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; - const isTypeSend = iouType === CONST.IOU.TYPE.SEND; - - const isSplitWithScan = isSplitBill && isScanRequest; - - 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 send money request - const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - - // In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited. For distance requests, don't show the merchant as there's already another "Distance" menu item - const shouldShowDate = shouldShowAllFields && !isTypeSend && !isSplitWithScan; - const shouldShowMerchant = shouldShowAllFields && !isTypeSend && !isDistanceRequest && !isSplitWithScan; - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - // A flag for showing the tags field - const shouldShowTags = isPolicyExpenseChat && (iouTag || OptionsListUtils.hasEnabledTags(policyTagLists)); - - // A flag for showing tax fields - tax rate and tax amount - const shouldShowTax = isPolicyExpenseChat && (policy?.tax?.trackingEnabled ?? policy?.isTaxTrackingEnabled); - - // A flag for showing the billable field - const shouldShowBillable = !policy?.disabledFields?.defaultBillable ?? true; - - const hasRoute = TransactionUtils.hasRoute(transaction ?? null); - const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !mileageRate?.rate); - const formattedAmount = isDistanceRequestWithPendingRoute - ? '' - : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount - ? DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0) - : iouAmount, - isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, - ); - const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); - - const defaultTaxKey = taxRates?.defaultExternalID; - const defaultTaxName = (defaultTaxKey && `${taxRates?.taxes[defaultTaxKey].name} (${taxRates?.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) ?? ''; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const taxRateTitle = transaction?.taxRate?.text || defaultTaxName; - - const isFocused = useIsFocused(); - const [formError, setFormError] = useState(null); - - const [didConfirm, setDidConfirm] = useState(false); - const [didConfirmSplit, setDidConfirmSplit] = useState(false); - - const shouldDisplayFieldError = useMemo(() => { - if (!isEditingSplitBill) { - return false; - } - - return (!!hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction ?? null)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction ?? null)); - }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); - - const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const shouldDisplayMerchantError = isPolicyExpenseChat && !isScanRequest && isMerchantEmpty; - - useEffect(() => { - if (shouldDisplayFieldError && didConfirmSplit) { - setFormError('iou.error.genericSmartscanFailureMessage'); - return; - } - if (shouldDisplayFieldError && hasSmartScanFailed) { - setFormError('iou.receiptScanningFailed'); - return; - } - // reset the form error whenever the screen gains or loses focus - setFormError(null); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); - - useEffect(() => { - if (!shouldCalculateDistanceAmount) { - return; - } - - const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0); - IOU.setMoneyRequestAmount(amount); - }, [shouldCalculateDistanceAmount, distance, mileageRate?.rate, mileageRate?.unit]); - - /** - * Returns the participants with amount - */ - const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const calculatedIouAmount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( - participantsList, - calculatedIouAmount > 0 ? CurrencyUtils.convertToDisplayString(calculatedIouAmount, 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); - } - - const splitOrRequestOptions: DropdownOption[] = useMemo(() => { - let text; - if (isSplitBill && iouAmount === 0) { - text = translate('iou.split'); - } else if (!!(receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { - text = translate('iou.request'); - if (iouAmount !== 0) { - text = translate('iou.requestAmount', {amount: formattedAmount}); - } - } else { - const translationKey = isSplitBill ? 'iou.splitAmount' : 'iou.requestAmount'; - text = translate(translationKey, {amount: formattedAmount}); - } - return [ - { - text: text[0].toUpperCase() + text.slice(1), - value: iouType, - }, - ]; - }, [isSplitBill, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); - - const selectedParticipantsMemo = useMemo(() => selectedParticipants.filter((participant) => participant.selected), [selectedParticipants]); - const payeePersonalDetailsMemo = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); - const canModifyParticipantsValue = !isReadOnly && canModifyParticipants && hasMultipleParticipants; - - const optionSelectorSections: CategorySection[] = useMemo(() => { - const sections = []; - const unselectedParticipants = selectedParticipants.filter((participant) => !participant.selected); - if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipantsMemo); - let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; - - if (!canModifyParticipantsValue) { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - } - - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetailsMemo, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - indexOffset: 0, - isDisabled: canModifyParticipantsValue, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - indexOffset: 1, - }, - ); - } else { - const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ - ...participant, - isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), - })); - sections.push({ - title: translate('common.to'), - data: formattedSelectedParticipants, - shouldShow: true, - indexOffset: 0, - }); - } - return sections; - }, [ - selectedParticipants, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - payeePersonalDetailsMemo, - translate, - canModifyParticipantsValue, - selectedParticipantsMemo, - ]); - - const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { - return []; - } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipantsMemo.length, iouAmount, iouCurrencyCode ?? '', true); - return [ - ...selectedParticipantsMemo, - OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetailsMemo, CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode)), - ]; - }, [hasMultipleParticipants, selectedParticipantsMemo, iouAmount, iouCurrencyCode, payeePersonalDetailsMemo]); - - useEffect(() => { - if (!isDistanceRequest) { - 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(transactionID ?? '', {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); - const distanceMerchant = DistanceRequestUtils.getDistanceMerchant( - hasRoute, - distance, - mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - mileageRate?.rate ?? 0, - mileageRate?.currency ?? 'USD', - translate, - toLocaleDigit, - ); - IOU.setMoneyRequestMerchant(transactionID ?? '', distanceMerchant, false); - }, [hasRoute, distance, mileageRate?.unit, mileageRate?.rate, mileageRate?.currency, translate, toLocaleDigit, isDistanceRequest, transactionID, isDistanceRequestWithPendingRoute]); - - 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: Participant | OnyxTypes.Report) => { - if ('accountID' in option && option.accountID) { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); - } else if ('reportID' in option && option.reportID) { - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(option.reportID)); - } - }; - - const confirm = useCallback( - (paymentMethod: PaymentMethodType | undefined) => { - if (selectedParticipantsMemo.length === 0) { - return; - } - if (iouCategory && iouCategory.length > CONST.API_TRANSACTION_CATEGORY_MAX_LENGTH) { - setFormError('iou.error.invalidCategoryLength'); - 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 requests - 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); - return; - } - - setDidConfirm(true); - onConfirm(selectedParticipantsMemo); - } - }, - [ - selectedParticipantsMemo, - iouCategory, - iouType, - onSendMoney, - iouCurrencyCode, - isDistanceRequest, - isDistanceRequestWithPendingRoute, - iouAmount, - isEditingSplitBill, - transaction, - onConfirm, - ], - ); - - const footerContent = useMemo(() => { - if (isReadOnly) { - return; - } - - const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipantsMemo.length === 0 || shouldDisplayMerchantError; - - const button = shouldShowSettlementButton ? ( - - ) : ( - confirm(value as PaymentMethodType)} - options={splitOrRequestOptions} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} - enterKeyEventListenerPriority={1} - /> - ); - - return ( - <> - {!!formError && ( - - )} - {button} - - ); - }, [ - isReadOnly, - iouType, - selectedParticipantsMemo.length, - shouldDisplayMerchantError, - confirm, - bankAccountRoute, - iouCurrencyCode, - policyID, - splitOrRequestOptions, - formError, - styles.ph1, - styles.mb2, - ]); - - const receiptData = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : null; - return ( - // @ts-expect-error TODO: Remove this once OptionsSelector (https://github.com/Expensify/App/issues/25125) is migrated to TypeScript. - - {isDistanceRequest && ( - - - - )} - {receiptData?.image ?? receiptData?.thumbnail ? ( - - ) : ( - // 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(), - ), - ) - } - /> - ) - )} - {shouldShowSmartScanFields && ( - { - 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_AMOUNT.getRoute(iouType, reportID)); - }} - style={[styles.moneyRequestMenuItem, styles.mt2]} - titleStyle={styles.moneyRequestConfirmationAmount} - disabled={didConfirm} - brickRoadIndicator={ - isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined - } - error={ - shouldDisplayMerchantError || (isPolicyExpenseChat && shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) - ? translate('common.error.enterMerchant') - : '' - } - /> - )} - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - style={styles.moneyRequestMenuItem} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - numberOfLinesTitle={2} - /> - {!shouldShowAllFields && ( - - )} - {shouldShowAllFields && ( - <> - {shouldShowDate && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction ?? null) ? translate('common.error.enterDate') : ''} - /> - )} - {isDistanceRequest && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))} - disabled={didConfirm || !isTypeRequest} - interactive={!isReadOnly} - /> - )} - {shouldShowMerchant && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - disabled={didConfirm} - interactive={!isReadOnly} - brickRoadIndicator={shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={ - shouldDisplayMerchantError || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction ?? null)) - ? translate('common.error.enterMerchant') - : '' - } - /> - )} - {shouldShowCategories && ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - style={styles.moneyRequestMenuItem} - titleStyle={styles.flex1} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={canUseViolations && Boolean(policy?.requiresCategory) ? translate('common.required') : ''} - /> - )} - {shouldShowTags && - policyTagLists.map(({name}, index) => ( - { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( - CONST.IOU.ACTION.EDIT, - CONST.IOU.TYPE.SPLIT, - index, - transaction?.transactionID ?? '', - reportID ?? '', - Navigation.getActiveRouteWithoutParams(), - ), - ); - }} - style={styles.moneyRequestMenuItem} - disabled={didConfirm} - interactive={!isReadOnly} - rightLabel={canUseViolations && !!policy?.requiresTag ? translate('common.required') : ''} - /> - ))} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - )} - - {shouldShowTax && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute(iouType, transaction?.transactionID ?? '', reportID ?? '', Navigation.getActiveRouteWithoutParams()), - ) - } - disabled={didConfirm} - interactive={!isReadOnly} - /> - )} - - {shouldShowBillable && ( - - {translate('common.billable')} - - - )} - - )} - - ); -} - -MoneyRequestConfirmationList.displayName = 'MoneyRequestConfirmationList'; - -export default withCurrentUserPersonalDetails( - withOnyx({ - policyCategories: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - }, - policyTags: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, - }, - mileageRate: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - selector: DistanceRequestUtils.getDefaultMileageRate, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - iou: { - key: ONYXKEYS.IOU, - }, - session: { - key: ONYXKEYS.SESSION, - }, - })(MoneyRequestConfirmationList), -); diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 83da817da858..a391ff061baa 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -18,7 +18,7 @@ import Text from './Text'; type PopoverMenuItem = { /** An icon element displayed on the left side */ - icon?: IconAsset; + icon: IconAsset; /** Text label */ text: string; diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index 6ca13a61933c..50bfcd4cc8be 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -228,10 +228,10 @@ function SettlementButton({ buttonRef={buttonRef} isDisabled={isDisabled} isLoading={isLoading} - onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType as PaymentMethodType, triggerKYCFlow)} + onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType, triggerKYCFlow)} pressOnEnter={pressOnEnter} options={paymentButtonOptions} - onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value as PaymentMethodType)} + onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value)} style={style} buttonSize={buttonSize} anchorAlignment={paymentMethodDropdownAnchorAlignment} diff --git a/src/languages/types.ts b/src/languages/types.ts index 975dd8fd1570..ed82bcd99a92 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -109,7 +109,7 @@ type RequestAmountParams = {amount: string}; type RequestedAmountMessageParams = {formattedAmount: string; comment?: string}; -type SplitAmountParams = {amount: string | number}; +type SplitAmountParams = {amount: number}; type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 155a167f322e..a42cb6a8f756 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,11 +1,17 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; -import type {MileageRate, Unit} from '@src/types/onyx/Policy'; +import type {Unit} from '@src/types/onyx/Policy'; import type Policy from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; +type DefaultMileageRate = { + rate?: number; + currency?: string; + unit: Unit; +}; + /** * Retrieves the default mileage rate based on a given policy. * @@ -16,7 +22,7 @@ import * as PolicyUtils from './PolicyUtils'; * @returns [currency] - The currency associated with the rate. * @returns [unit] - The unit of measurement for the distance. */ -function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { +function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | null { if (!policy?.customUnits) { return null; } @@ -33,7 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { return { rate: distanceRate.rate, - currency: distanceRate.currency ?? 'USD', + currency: distanceRate.currency, unit: distanceUnit.attributes.unit, }; } diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 3c003ab03590..f03c34b1696e 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -1,6 +1,5 @@ import {format, lastDayOfMonth, setDate} from 'date-fns'; import Str from 'expensify-common/lib/str'; -import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -64,7 +63,7 @@ type BuildNextStepParameters = { * @returns nextStep */ function buildNextStep( - report: OnyxEntry | EmptyObject, + report: Report | EmptyObject, predictedNextStatus: ValueOf, {isPaidWithExpensify}: BuildNextStepParameters = {}, ): ReportNextStep | null { @@ -72,13 +71,13 @@ function buildNextStep( return null; } - const {policyID = '', ownerAccountID = -1, managerID = -1} = report ?? {}; + const {policyID = '', ownerAccountID = -1, managerID = -1} = report; const policy = ReportUtils.getPolicy(policyID); const {submitsTo, harvesting, isPreventSelfApprovalEnabled, preventSelfApprovalEnabled, autoReportingFrequency, autoReportingOffset} = policy; const isOwner = currentUserAccountID === ownerAccountID; const isManager = currentUserAccountID === managerID; const isSelfApproval = currentUserAccountID === submitsTo; - const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([report?.ownerAccountID ?? -1])[0] ?? ''; + const ownerLogin = PersonalDetailsUtils.getLoginsByAccountIDs([ownerAccountID])[0] ?? ''; const managerDisplayName = isSelfApproval ? 'you' : ReportUtils.getDisplayNameForParticipant(submitsTo) ?? ''; const type: ReportNextStep['type'] = 'neutral'; let optimisticNextStep: ReportNextStep | null; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 0bf55edd260d..9121eebb3367 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -31,7 +31,6 @@ import type { import type {Participant} from '@src/types/onyx/IOU'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import times from '@src/utils/times'; import Timing from './actions/Timing'; @@ -1744,7 +1743,7 @@ function getShareLogOptions(reports: OnyxCollection, personalDetails: On /** * Build the IOUConfirmation options for showing the payee personalDetail */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText: string): PayeePersonalDetails { +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string): PayeePersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 2e1a283bc8b8..67e31c610369 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -140,11 +140,11 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean { return !!transaction?.receipt?.state || hasEReceipt(transaction); } -function isMerchantMissing(transaction: OnyxEntry) { - if (transaction?.modifiedMerchant && transaction?.modifiedMerchant !== '') { +function isMerchantMissing(transaction: Transaction) { + if (transaction.modifiedMerchant && transaction.modifiedMerchant !== '') { return transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; } - const isMerchantEmpty = transaction?.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction?.merchant === ''; + const isMerchantEmpty = transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === ''; return isMerchantEmpty; } @@ -156,15 +156,15 @@ function isPartialMerchant(merchant: string): boolean { return merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; } -function isAmountMissing(transaction: OnyxEntry) { - return transaction?.amount === 0 && (!transaction?.modifiedAmount || transaction?.modifiedAmount === 0); +function isAmountMissing(transaction: Transaction) { + return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0); } -function isCreatedMissing(transaction: OnyxEntry) { - return transaction?.created === '' && (!transaction?.created || transaction?.modifiedCreated === ''); +function isCreatedMissing(transaction: Transaction) { + return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === ''); } -function areRequiredFieldsEmpty(transaction: OnyxEntry): boolean { +function areRequiredFieldsEmpty(transaction: Transaction): boolean { const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`] ?? null; const isFromExpenseReport = parentReport?.type === CONST.REPORT.TYPE.EXPENSE; return (isFromExpenseReport && isMerchantMissing(transaction)) || isAmountMissing(transaction) || isCreatedMissing(transaction); @@ -487,7 +487,7 @@ function hasMissingSmartscanFields(transaction: OnyxEntry): boolean /** * Check if the transaction has a defined route */ -function hasRoute(transaction: OnyxEntry): boolean { +function hasRoute(transaction: Transaction): boolean { return !!transaction?.routes?.route0?.geometry?.coordinates; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ff7fa1d1d352..dbab05dcfcd1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3673,14 +3673,14 @@ function sendMoneyWithWallet(report: OnyxTypes.Report, amount: number, currency: Report.notifyNewAction(params.chatReportID, managerID); } -function approveMoneyRequest(expenseReport: OnyxEntry | EmptyObject) { - const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`] ?? null; - const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport?.total ?? 0, expenseReport?.currency ?? '', expenseReport?.reportID ?? ''); +function approveMoneyRequest(expenseReport: OnyxTypes.Report | EmptyObject) { + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; + const optimisticApprovedReportAction = ReportUtils.buildOptimisticApprovedReportAction(expenseReport.total ?? 0, expenseReport.currency ?? '', expenseReport.reportID); const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, CONST.REPORT.STATUS_NUM.APPROVED); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { ...(optimisticApprovedReportAction as OnyxTypes.ReportAction), @@ -3690,7 +3690,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry | EmptyO }; const optimisticIOUReportData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, value: { ...expenseReport, lastMessageText: optimisticApprovedReportAction.message?.[0].text, @@ -3701,7 +3701,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry | EmptyO }; const optimisticNextStepData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: optimisticNextStep, }; const optimisticData: OnyxUpdate[] = [optimisticIOUReportData, optimisticReportActionsData, optimisticNextStepData]; @@ -3709,7 +3709,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry | EmptyO const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { [optimisticApprovedReportAction.reportActionID]: { pendingAction: null, @@ -3721,22 +3721,22 @@ function approveMoneyRequest(expenseReport: OnyxEntry | EmptyO const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, value: { - [expenseReport?.reportActionID ?? '']: { + [expenseReport.reportActionID ?? '']: { errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), }, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`, value: currentNextStep, }, ]; const parameters: ApproveMoneyRequestParams = { - reportID: expenseReport?.reportID ?? '', + reportID: expenseReport.reportID, approvedReportActionID: optimisticApprovedReportAction.reportActionID, }; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 198c606cf9f2..06c2d2e6abce 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -3,6 +3,7 @@ import type CONST from '@src/CONST'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; type PaymentMethodType = DeepValueOf; + type ActionName = DeepValueOf; type OriginalMessageActionName = | 'ADDCOMMENT' diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 1eece2d3a1e0..fc07e1e1760a 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -17,12 +17,6 @@ type Attributes = { unit: Unit; }; -type MileageRate = { - unit: Unit; - rate?: number; - currency: string; -}; - type CustomUnit = OnyxCommon.OnyxValueWithOfflineFeedback<{ name: string; customUnitID: string; @@ -253,4 +247,4 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< export default Policy; -export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault, MileageRate}; +export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault};