diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 01539b0d5776..36b11ff58b40 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -11,7 +11,20 @@ import {MouseProvider} from '@hooks/useMouseContext'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; -// +import { + adjustRemainingSplitShares, + resetSplitShares, + setCustomUnitRateID, + setIndividualShare, + setMoneyRequestAmount, + setMoneyRequestCategory, + setMoneyRequestMerchant, + setMoneyRequestPendingFields, + setMoneyRequestTag, + setMoneyRequestTaxAmount, + setMoneyRequestTaxRate, + setSplitShares, +} from '@libs/actions/IOU'; import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils'; @@ -36,20 +49,6 @@ import { isMerchantMissing, isScanRequest as isScanRequestUtil, } from '@libs/TransactionUtils'; -import { - adjustRemainingSplitShares, - resetSplitShares, - setCustomUnitRateID, - setIndividualShare, - setMoneyRequestAmount, - setMoneyRequestCategory, - setMoneyRequestMerchant, - setMoneyRequestPendingFields, - setMoneyRequestTag, - setMoneyRequestTaxAmount, - setMoneyRequestTaxRate, - setSplitShares, -} from '@userActions/IOU'; import {hasInvoicingDetails} from '@userActions/Policy/Policy'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; diff --git a/src/languages/en.ts b/src/languages/en.ts index 9147d93c4c45..30b3fb901843 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -846,7 +846,7 @@ const translations = { assignTask: 'Assign task', header: 'Quick action', trackManual: 'Create expense', - trackScan: 'Create expense for receipt', + trackScan: 'Scan receipt', trackDistance: 'Track distance', noLongerHaveReportAccess: 'You no longer have access to your previous quick action destination. Pick a new one below.', updateDestination: 'Update destination', @@ -1078,7 +1078,6 @@ const translations = { bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.', attendees: 'Attendees', paymentComplete: 'Payment complete', - justTrackIt: 'Just track it (don’t submit it)', time: 'Time', startDate: 'Start date', endDate: 'End date', diff --git a/src/languages/es.ts b/src/languages/es.ts index 48ad885478d3..d332fcabbeec 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -841,7 +841,7 @@ const translations = { assignTask: 'Assignar tarea', header: 'Acción rápida', trackManual: 'Crear gasto', - trackScan: 'Crear gasto por recibo', + trackScan: 'Escanear recibo', trackDistance: 'Crear gasto por desplazamiento', noLongerHaveReportAccess: 'Ya no tienes acceso al destino previo de esta acción rápida. Escoge uno nuevo a continuación.', updateDestination: 'Actualiza el destino', @@ -1076,7 +1076,6 @@ const translations = { bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.', attendees: 'Asistentes', paymentComplete: 'Pago completo', - justTrackIt: 'Solo guardarlo (no enviarlo)', time: 'Tiempo', startDate: 'Fecha de inicio', endDate: 'Fecha de finalización', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 4b95fd1ed3b3..75808914d72f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -200,6 +200,8 @@ type GetOptionsConfig = { includeRecentReports?: boolean; includeSelectedOptions?: boolean; recentAttendees?: Attendee[]; + shouldSeparateWorkspaceChat?: boolean; + shouldSeparateSelfDMChat?: boolean; } & GetValidReportsConfig; type GetUserToInviteConfig = { @@ -230,6 +232,8 @@ type Options = { personalDetails: OptionData[]; userToInvite: OptionData | null; currentUserOption: OptionData | null | undefined; + workspaceChats?: OptionData[]; + selfDMChat?: OptionData | undefined; }; type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean}; @@ -257,7 +261,7 @@ type OrderReportOptionsConfig = { preferRecentExpenseReports?: boolean; }; -type ReportAndPersonalDetailOptions = Pick; +type ReportAndPersonalDetailOptions = Pick; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1104,6 +1108,22 @@ function orderReportOptionsWithSearch( ); } +function orderWorkspaceOptions(options: OptionData[]): OptionData[] { + return options.sort((a, b) => { + // Check if `a` is the default workspace + if (a.isPolicyExpenseChat && a.policyID === activePolicyID) { + return -1; + } + + // Check if `b` is the default workspace + if (b.isPolicyExpenseChat && b.policyID === activePolicyID) { + return 1; + } + + return 0; + }); +} + function sortComparatorReportOptionByArchivedStatus(option: OptionData) { return option.private_isArchived ? 1 : 0; } @@ -1131,10 +1151,12 @@ function orderOptions(options: ReportAndPersonalDetailOptions, searchValue?: str orderedReportOptions = orderReportOptions(options.recentReports); } const orderedPersonalDetailsOptions = orderPersonalDetailsOptions(options.personalDetails); + const orderedWorkspaceChats = orderWorkspaceOptions(options?.workspaceChats ?? []); return { recentReports: orderedReportOptions, personalDetails: orderedPersonalDetailsOptions, + workspaceChats: orderedWorkspaceChats, }; } @@ -1388,7 +1410,16 @@ function getValidReports( */ function getValidOptions( options: OptionList, - {excludeLogins = [], includeSelectedOptions = false, includeRecentReports = true, recentAttendees, selectedOptions = [], ...config}: GetOptionsConfig = {}, + { + excludeLogins = [], + includeSelectedOptions = false, + includeRecentReports = true, + recentAttendees, + selectedOptions = [], + shouldSeparateSelfDMChat = false, + shouldSeparateWorkspaceChat = false, + ...config + }: GetOptionsConfig = {}, ): Options { // Gather shared configs: const optionsToExclude: Option[] = [{login: CONST.EMAIL.NOTIFICATIONS}]; @@ -1445,6 +1476,22 @@ function getValidOptions( } } + let workspaceChats: OptionData[] = []; + + if (shouldSeparateWorkspaceChat) { + workspaceChats = recentReportOptions.filter((option) => option.isOwnPolicyExpenseChat && !option.private_isArchived); + } + + let selfDMChat: OptionData | undefined; + + if (shouldSeparateWorkspaceChat) { + recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat); + } + if (shouldSeparateSelfDMChat) { + selfDMChat = recentReportOptions.find((option) => option.isSelfDM); + recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); + } + return { personalDetails: personalDetailsOptions, recentReports: recentReportOptions, @@ -1452,6 +1499,8 @@ function getValidOptions( // User to invite is generated by the search input of a user. // As this function isn't concerned with any search input yet, this is null (will be set when using filterOptions). userToInvite: null, + workspaceChats, + selfDMChat, }; } @@ -1691,6 +1740,7 @@ function formatSectionsFromSearchTerm( filteredPersonalDetails: OptionData[], personalDetails: OnyxEntry = {}, shouldGetOptionDetails = false, + filteredWorkspaceChats: OptionData[] = [], ): SectionForSearchTerm { // We show the selected participants at the top of the list when there is no search term or maximum number of participants has already been selected // However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results @@ -1716,8 +1766,9 @@ function formatSectionsFromSearchTerm( const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => { const accountID = participant.accountID ?? null; const isPartOfSearchTerm = getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm); - const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID); + const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID) || filteredWorkspaceChats.some((report) => report.accountID === accountID); const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID); + return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails; }); @@ -1803,6 +1854,23 @@ function filterReports(reports: OptionData[], searchTerms: string[]): OptionData return filteredReports; } +function filterWorkspaceChats(reports: OptionData[], searchTerms: string[]): OptionData[] { + const filteredReports = searchTerms.reduceRight( + (items, term) => + filterArrayByMatch(items, term, (item) => { + const values: string[] = []; + if (item.text) { + values.push(item.text); + } + return uniqFast(values); + }), + // We start from all unfiltered reports: + reports, + ); + + return filteredReports; +} + function filterPersonalDetails(personalDetails: OptionData[], searchTerms: string[]): OptionData[] { return searchTerms.reduceRight( (items, term) => @@ -1854,6 +1922,34 @@ function filterUserToInvite(options: Omit, searchValue: }); } +function filterSelfDMChat(report: OptionData, searchTerms: string[]): OptionData | undefined { + const isMatch = searchTerms.every((term) => { + const values: string[] = []; + + if (report.text) { + values.push(report.text); + } + if (report.login) { + values.push(report.login); + values.push(report.login.replace(CONST.EMAIL_SEARCH_REGEX, '')); + } + if (report.isThread) { + if (report.alternateText) { + values.push(report.alternateText); + } + } else if (!!report.isChatRoom || !!report.isPolicyExpenseChat) { + if (report.subtitle) { + values.push(report.subtitle); + } + } + + // Remove duplicate values and check if the term matches any value + return uniqFast(values).some((value) => value.includes(term)); + }); + + return isMatch ? report : undefined; +} + function filterOptions(options: Options, searchInputValue: string, config?: FilterUserToInviteConfig): Options { const parsedPhoneNumber = parsePhoneNumber(appendCountryCode(Str.removeSMSDomain(searchInputValue))); const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); @@ -1871,12 +1967,17 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt searchValue, config, ); + const workspaceChats = filterWorkspaceChats(options.workspaceChats ?? [], searchTerms); + + const selfDMChat = options.selfDMChat ? filterSelfDMChat(options.selfDMChat, searchTerms) : undefined; return { personalDetails, recentReports, userToInvite, currentUserOption, + workspaceChats, + selfDMChat, }; } diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 07e34d9692b1..5f9cb87d15b8 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -7,8 +7,6 @@ import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import EmptySelectionListContent from '@components/EmptySelectionListContent'; import FormHelpMessage from '@components/FormHelpMessage'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MenuItem from '@components/MenuItem'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useOptionsList} from '@components/OptionListContextProvider'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; @@ -21,19 +19,32 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as SubscriptionUtils from '@libs/SubscriptionUtils'; -import * as Policy from '@userActions/Policy/Policy'; -import * as Report from '@userActions/Report'; +import { + filterAndOrderOptions, + formatSectionsFromSearchTerm, + getHeaderMessage, + getParticipantsOption, + getPersonalDetailSearchTerms, + getPolicyExpenseReportOption, + getValidOptions, + isCurrentUser, + orderOptions, +} from '@libs/OptionsListUtils'; +import type {Section} from '@libs/OptionsListUtils'; +import {isPaidGroupPolicy as isPaidGroupPolicyUtil} from '@libs/PolicyUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import {isInvoiceRoom} from '@libs/ReportUtils'; +import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {getInvoicePrimaryWorkspace} from '@userActions/Policy/Policy'; +import {searchInServer} from '@userActions/Report'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Participant} from '@src/types/onyx/IOU'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MoneyRequestParticipantsSelectorProps = { /** Callback to request parent modal to go to next step, which should be split */ @@ -42,9 +53,6 @@ type MoneyRequestParticipantsSelectorProps = { /** Callback to add participants in MoneyRequestModal */ onParticipantsAdded: (value: Participant[]) => void; - /** Callback to navigate to Track Expense confirmation flow */ - onTrackExpensePress?: () => void; - /** Selected participants from MoneyRequestModal with login */ participants?: Participant[] | typeof CONST.EMPTY_ARRAY; @@ -53,20 +61,9 @@ type MoneyRequestParticipantsSelectorProps = { /** The action of the IOU, i.e. create, split, move */ action: IOUAction; - - /** Whether we should display the Track Expense button at the top of the participants list */ - shouldDisplayTrackExpenseButton?: boolean; }; -function MoneyRequestParticipantsSelector({ - participants = CONST.EMPTY_ARRAY, - onTrackExpensePress, - onFinish, - onParticipantsAdded, - iouType, - action, - shouldDisplayTrackExpenseButton, -}: MoneyRequestParticipantsSelectorProps) { +function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, action}: MoneyRequestParticipantsSelectorProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -86,12 +83,12 @@ function MoneyRequestParticipantsSelector({ const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]); const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - const isPaidGroupPolicy = useMemo(() => PolicyUtils.isPaidGroupPolicy(policy), [policy]); + const isPaidGroupPolicy = useMemo(() => isPaidGroupPolicyUtil(policy), [policy]); const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT; const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action); useEffect(() => { - Report.searchInServer(debouncedSearchTerm.trim()); + searchInServer(debouncedSearchTerm.trim()); }, [debouncedSearchTerm]); const defaultOptions = useMemo(() => { @@ -105,7 +102,7 @@ function MoneyRequestParticipantsSelector({ }; } - const optionList = OptionsListUtils.getValidOptions( + const optionList = getValidOptions( { reports: options.reports, personalDetails: options.personalDetails, @@ -123,10 +120,13 @@ function MoneyRequestParticipantsSelector({ includeP2P: !isCategorizeOrShareAction, includeInvoiceRooms: iouType === CONST.IOU.TYPE.INVOICE, action, + shouldSeparateSelfDMChat: iouType !== CONST.IOU.TYPE.INVOICE, + shouldSeparateWorkspaceChat: true, + includeSelfDM: true, }, ); - const orderedOptions = OptionsListUtils.orderOptions(optionList); + const orderedOptions = orderOptions(optionList); return { ...optionList, @@ -142,10 +142,12 @@ function MoneyRequestParticipantsSelector({ personalDetails: [], currentUserOption: null, headerMessage: '', + workspaceChats: [], + selfDMChat: null, }; } - const newOptions = OptionsListUtils.filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { + const newOptions = filterAndOrderOptions(defaultOptions, debouncedSearchTerm, { canInviteUser: !isCategorizeOrShareAction, selectedOptions: participants as Participant[], excludeLogins: CONST.EXPENSIFY_EMAILS, @@ -161,14 +163,14 @@ function MoneyRequestParticipantsSelector({ * @returns {Array} */ const [sections, header] = useMemo(() => { - const newSections: OptionsListUtils.Section[] = []; + const newSections: Section[] = []; if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, '']; } - const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( + const formatResults = formatSectionsFromSearchTerm( debouncedSearchTerm, - participants.map((participant) => ({...participant, reportID: participant.reportID ?? '-1'})), + participants.map((participant) => ({...participant, reportID: participant.reportID})) as OptionData[], chatOptions.recentReports, chatOptions.personalDetails, personalDetails, @@ -177,6 +179,18 @@ function MoneyRequestParticipantsSelector({ newSections.push(formatResults.section); + newSections.push({ + title: translate('workspace.common.workspace'), + data: chatOptions.workspaceChats ?? [], + shouldShow: (chatOptions.workspaceChats ?? []).length > 0, + }); + + newSections.push({ + title: translate('workspace.invoices.paymentMethods.personal'), + data: chatOptions.selfDMChat ? [chatOptions.selfDMChat] : [], + shouldShow: !!chatOptions.selfDMChat, + }); + newSections.push({ title: translate('common.recents'), data: chatOptions.recentReports, @@ -191,23 +205,27 @@ function MoneyRequestParticipantsSelector({ if ( chatOptions.userToInvite && - !OptionsListUtils.isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? -1, status: chatOptions.userToInvite?.status ?? undefined}) + !isCurrentUser({ + ...chatOptions.userToInvite, + accountID: chatOptions.userToInvite?.accountID ?? CONST.DEFAULT_NUMBER_ID, + status: chatOptions.userToInvite?.status ?? undefined, + }) ) { newSections.push({ title: undefined, data: [chatOptions.userToInvite].map((participant) => { const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false; - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + return isPolicyExpenseChat ? getPolicyExpenseReportOption(participant) : getParticipantsOption(participant, personalDetails); }), shouldShow: true, }); } - const headerMessage = OptionsListUtils.getHeaderMessage( - (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0, + const headerMessage = getHeaderMessage( + (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length + (chatOptions.workspaceChats ?? []).length !== 0 || !isEmptyObject(chatOptions.selfDMChat), !!chatOptions?.userToInvite, debouncedSearchTerm.trim(), - participants.some((participant) => OptionsListUtils.getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), + participants.some((participant) => getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm)), ); return [newSections, headerMessage]; @@ -218,6 +236,8 @@ function MoneyRequestParticipantsSelector({ participants, chatOptions.recentReports, chatOptions.personalDetails, + chatOptions.selfDMChat, + chatOptions.workspaceChats, chatOptions.userToInvite, personalDetails, translate, @@ -233,14 +253,14 @@ function MoneyRequestParticipantsSelector({ (option: Participant) => { const newParticipants: Participant[] = [ { - ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), + ...lodashPick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID', 'isSelfDM', 'text', 'phoneNumber'), selected: true, iouType, }, ]; if (iouType === CONST.IOU.TYPE.INVOICE) { - const policyID = option.item && ReportUtils.isInvoiceRoom(option.item) ? option.policyID : Policy.getInvoicePrimaryWorkspace(currentUserLogin)?.id; + const policyID = option.item && isInvoiceRoom(option.item) ? option.policyID : getInvoicePrimaryWorkspace(currentUserLogin)?.id; newParticipants.push({ policyID, isSender: true, @@ -250,7 +270,10 @@ function MoneyRequestParticipantsSelector({ } onParticipantsAdded(newParticipants); - onFinish(); + + if (!option.isSelfDM) { + onFinish(); + } }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes [onFinish, onParticipantsAdded, currentUserLogin], @@ -343,22 +366,6 @@ function MoneyRequestParticipantsSelector({ const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent; - const headerContent = useMemo(() => { - if (!shouldDisplayTrackExpenseButton) { - return; - } - - // We only display the track expense button if the user is coming from the combined submit/track flow. - return ( - - ); - }, [shouldDisplayTrackExpenseButton, translate, onTrackExpensePress]); - const footerContent = useMemo(() => { if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { return; @@ -417,7 +424,7 @@ function MoneyRequestParticipantsSelector({ const onSelectRow = useCallback( (option: Participant) => { - if (option.isPolicyExpenseChat && option.policyID && SubscriptionUtils.shouldRestrictUserBillableActions(option.policyID)) { + if (option.isPolicyExpenseChat && option.policyID && shouldRestrictUserBillableActions(option.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(option.policyID)); return; } @@ -441,10 +448,9 @@ function MoneyRequestParticipantsSelector({ textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} textInputHint={offlineMessage} onChangeText={setSearchTerm} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} onSelectRow={onSelectRow} shouldSingleExecuteRowSelect - headerContent={headerContent} footerContent={footerContent} listEmptyContent={} headerMessage={header} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 6a67a1040f1b..c9a4353d854a 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -5,16 +5,24 @@ import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {READ_COMMANDS} from '@libs/API/types'; -import * as Browser from '@libs/Browser'; +import {isMobileSafari as isMobileSafariUtil} from '@libs/Browser'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getPlatform from '@libs/getPlatform'; import HttpUtils from '@libs/HttpUtils'; -import * as IOUUtils from '@libs/IOUUtils'; +import {isMovingTransactionFromTrackExpense, navigateToStartMoneyRequestStep} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {createDraftWorkspaceAndNavigateToConfirmationScreen, findSelfDMReportID, isInvoiceRoomWithID} from '@libs/ReportUtils'; +import {getRequestType} from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestParticipantsSelector'; -import * as IOU from '@userActions/IOU'; +import { + navigateToStartStepIfScanFileCannotBeRead, + setCustomUnitRateID, + setMoneyRequestCategory, + setMoneyRequestParticipants, + setMoneyRequestParticipantsFromReport, + setMoneyRequestTag, + setSplitShares, +} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -46,7 +54,7 @@ function IOURequestStepParticipants({ // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID); const numberOfParticipants = useRef(participants?.length ?? 0); - const iouRequestType = TransactionUtils.getRequestType(transaction); + const iouRequestType = getRequestType(transaction); const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; const headerTitle = useMemo(() => { if (action === CONST.IOU.ACTION.CATEGORIZE) { @@ -67,38 +75,58 @@ function IOURequestStepParticipants({ return translate('iou.chooseRecipient'); }, [iouType, translate, isSplitRequest, action]); - const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), []); + const selfDMReportID = useMemo(() => findSelfDMReportID(), []); const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); - const shouldDisplayTrackExpenseButton = !!selfDMReportID && iouType === CONST.IOU.TYPE.CREATE; const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; const receiptType = transaction?.receipt?.type; const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; - const isMobileSafari = Browser.isMobileSafari(); + const isMobileSafari = isMobileSafariUtil(); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the expense is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then // the image ceases to exist. The best way for the user to recover from this is to start over from the start of the expense process. // skip this in case user is moving the transaction as the receipt path will be valid in that case useEffect(() => { - if (IOUUtils.isMovingTransactionFromTrackExpense(action)) { + if (isMovingTransactionFromTrackExpense(action)) { return; } - IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename ?? '', receiptPath ?? '', () => {}, iouRequestType, iouType, transactionID, reportID, receiptType ?? ''); + navigateToStartStepIfScanFileCannotBeRead(receiptFilename ?? '', receiptPath ?? '', () => {}, iouRequestType, iouType, transactionID, reportID, receiptType ?? ''); }, [receiptType, receiptPath, receiptFilename, iouRequestType, iouType, transactionID, reportID, action]); + const trackExpense = useCallback(() => { + // If coming from the combined submit/track flow and the user proceeds to just track the expense, + // we will use the track IOU type in the confirmation flow. + if (!selfDMReportID) { + return; + } + + const rateID = DistanceRequestUtils.getCustomUnitRateID(selfDMReportID); + setCustomUnitRateID(transactionID, rateID); + setMoneyRequestParticipantsFromReport(transactionID, selfDMReport); + const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); + Navigation.navigate(iouConfirmationPageRoute); + }, [action, selfDMReport, selfDMReportID, transactionID]); + const addParticipant = useCallback( (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); + const firstParticipant = val.at(0); + + if (firstParticipant?.isSelfDM) { + trackExpense(); + return; + } + const firstParticipantReportID = val.at(0)?.reportID; const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID); - const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID); + const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; - IOU.setMoneyRequestParticipants(transactionID, val); - IOU.setCustomUnitRateID(transactionID, rateID); + setMoneyRequestParticipants(transactionID, val); + setCustomUnitRateID(transactionID, rateID); // When multiple participants are selected, the reportID is generated at the end of the confirmation step. // So we are resetting selectedReportID ref to the reportID coming from params. @@ -110,7 +138,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = firstParticipantReportID ?? reportID; }, - [iouType, reportID, transactionID], + [iouType, reportID, trackExpense, transactionID], ); const handleNavigation = useCallback( @@ -133,13 +161,13 @@ function IOURequestStepParticipants({ const isPolicyExpenseChat = participants?.some((participant) => participant.isPolicyExpenseChat); if (iouType === CONST.IOU.TYPE.SPLIT && !isPolicyExpenseChat && transaction?.amount && transaction?.currency) { const participantAccountIDs = participants?.map((participant) => participant.accountID) as number[]; - IOU.setSplitShares(transaction, transaction.amount, transaction.currency, participantAccountIDs); + setSplitShares(transaction, transaction.amount, transaction.currency, participantAccountIDs); } - IOU.setMoneyRequestTag(transactionID, ''); - IOU.setMoneyRequestCategory(transactionID, ''); + setMoneyRequestTag(transactionID, ''); + setMoneyRequestCategory(transactionID, ''); if ((isCategorizing || isShareAction) && numberOfParticipants.current === 0) { - ReportUtils.createDraftWorkspaceAndNavigateToConfirmationScreen(transactionID, action); + createDraftWorkspaceAndNavigateToConfirmationScreen(transactionID, action); return; } @@ -160,29 +188,14 @@ function IOURequestStepParticipants({ }, [action, participants, iouType, transaction, transactionID, reportID, handleNavigation]); const navigateBack = useCallback(() => { - IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action); + navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action); }, [iouRequestType, iouType, transactionID, reportID, action]); - const trackExpense = () => { - // If coming from the combined submit/track flow and the user proceeds to just track the expense, - // we will use the track IOU type in the confirmation flow. - if (!selfDMReportID) { - return; - } - - const rateID = DistanceRequestUtils.getCustomUnitRateID(selfDMReportID); - IOU.setCustomUnitRateID(transactionID, rateID); - IOU.setMoneyRequestParticipantsFromReport(transactionID, selfDMReport); - const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - - handleNavigation(iouConfirmationPageRoute); - }; - useEffect(() => { const isCategorizing = action === CONST.IOU.ACTION.CATEGORIZE; const isShareAction = action === CONST.IOU.ACTION.SHARE; if (isFocused && (isCategorizing || isShareAction)) { - IOU.setMoneyRequestParticipants(transactionID, []); + setMoneyRequestParticipants(transactionID, []); numberOfParticipants.current = 0; } }, [isFocused, action, transactionID]); @@ -206,10 +219,8 @@ function IOURequestStepParticipants({ participants={isSplitRequest ? participants : []} onParticipantsAdded={addParticipant} onFinish={goToNextStep} - onTrackExpensePress={trackExpense} iouType={iouType} action={action} - shouldDisplayTrackExpenseButton={shouldDisplayTrackExpenseButton} /> );