diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index 9b6875ccf8c2..5a40c28a86c9 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -174,7 +174,11 @@ function BaseOptionsList({ const renderItem = ({item, index, section}) => { const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; const isSelected = _.some(selectedOptions, (option) => { - if (option.accountID === item.accountID) { + if (option.accountID && option.accountID === item.accountID) { + return true; + } + + if (option.reportID && option.reportID === item.reportID) { return true; } diff --git a/src/libs/MoneyRequestUtils.ts b/src/libs/MoneyRequestUtils.ts index 37f72aa10a76..b8a6a3da303f 100644 --- a/src/libs/MoneyRequestUtils.ts +++ b/src/libs/MoneyRequestUtils.ts @@ -71,4 +71,11 @@ function isDistanceRequest(iouType: ValueOf return iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST && selectedTab === CONST.TAB.DISTANCE; } -export {stripCommaFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits, isDistanceRequest}; +/** + * Check if scan request or not + */ +function isScanRequest(selectedTab: ValueOf): boolean { + return selectedTab === CONST.TAB.SCAN; +} + +export {stripCommaFromAmount, stripSpacesFromAmount, addLeadingZero, validateAmount, replaceAllDigits, isDistanceRequest, isScanRequest}; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 8705d9f78004..8587ae1933db 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -92,31 +92,29 @@ Onyx.connect({ }); /** - * Get the options for a policy expense report. + * Get the option for a policy expense report. * @param {Object} report - * @returns {Array} + * @returns {Object} */ -function getPolicyExpenseReportOptions(report) { +function getPolicyExpenseReportOption(report) { const expenseReport = policyExpenseReports[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; const policyExpenseChatAvatarSource = ReportUtils.getWorkspaceAvatar(expenseReport); const reportName = ReportUtils.getReportName(expenseReport); - return [ - { - ...expenseReport, - keyForList: expenseReport.policyID, - text: reportName, - alternateText: Localize.translateLocal('workspace.common.workspace'), - icons: [ - { - source: policyExpenseChatAvatarSource, - name: reportName, - type: CONST.ICON_TYPE_WORKSPACE, - }, - ], - selected: report.selected, - isPolicyExpenseChat: true, - }, - ]; + return { + ...expenseReport, + keyForList: expenseReport.policyID, + text: reportName, + alternateText: Localize.translateLocal('workspace.common.workspace'), + icons: [ + { + source: policyExpenseChatAvatarSource, + name: reportName, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + selected: report.selected, + isPolicyExpenseChat: true, + }; } /** @@ -201,37 +199,34 @@ function isPersonalDetailsReady(personalDetails) { } /** - * Get the participant options for a report. - * @param {Array} participants + * Get the participant option for a report. + * @param {Object} participant * @param {Array} personalDetails - * @returns {Array} + * @returns {Object} */ -function getParticipantsOptions(participants, personalDetails) { - const details = getPersonalDetailsForAccountIDs(_.pluck(participants, 'accountID'), personalDetails); - return _.map(participants, (participant) => { - const detail = details[participant.accountID]; - const login = detail.login || participant.login; - const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); - return { - keyForList: String(detail.accountID), - login, - accountID: detail.accountID, - text: displayName, - firstName: lodashGet(detail, 'firstName', ''), - lastName: lodashGet(detail, 'lastName', ''), - alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, - icons: [ - { - source: UserUtils.getAvatar(detail.avatar, detail.accountID), - name: login, - type: CONST.ICON_TYPE_AVATAR, - id: detail.accountID, - }, - ], - phoneNumber: lodashGet(detail, 'phoneNumber', ''), - selected: participant.selected, - }; - }); +function getParticipantsOption(participant, personalDetails) { + const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; + const login = detail.login || participant.login; + const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + return { + keyForList: String(detail.accountID), + login, + accountID: detail.accountID, + text: displayName, + firstName: lodashGet(detail, 'firstName', ''), + lastName: lodashGet(detail, 'lastName', ''), + alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, + icons: [ + { + source: UserUtils.getAvatar(detail.avatar, detail.accountID), + name: login, + type: CONST.ICON_TYPE_AVATAR, + id: detail.accountID, + }, + ], + phoneNumber: lodashGet(detail, 'phoneNumber', ''), + selected: participant.selected, + }; } /** @@ -895,10 +890,10 @@ function getOptions( } // Always exclude already selected options and the currently logged in user - const loginOptionsToExclude = [...selectedOptions, {login: currentUserLogin}]; + const optionsToExclude = [...selectedOptions, {login: currentUserLogin}]; _.each(excludeLogins, (login) => { - loginOptionsToExclude.push({login}); + optionsToExclude.push({login}); }); if (includeRecentReports) { @@ -919,7 +914,11 @@ function getOptions( } // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected - if (!includeThreads && reportOption.login && _.some(loginOptionsToExclude, (option) => option.login === reportOption.login)) { + if ( + !includeThreads && + (reportOption.login || reportOption.reportID) && + _.some(optionsToExclude, (option) => (option.login && option.login === reportOption.login) || (option.reportID && option.reportID === reportOption.reportID)) + ) { continue; } @@ -943,7 +942,7 @@ function getOptions( // Add this login to the exclude list so it won't appear when we process the personal details if (reportOption.login) { - loginOptionsToExclude.push({login: reportOption.login}); + optionsToExclude.push({login: reportOption.login}); } } } @@ -951,7 +950,7 @@ function getOptions( if (includePersonalDetails) { // Next loop over all personal details removing any that are selectedUsers or recentChats _.each(allPersonalDetailsOptions, (personalDetailOption) => { - if (_.some(loginOptionsToExclude, (loginOptionToExclude) => loginOptionToExclude.login === personalDetailOption.login)) { + if (_.some(optionsToExclude, (optionToExclude) => optionToExclude.login === personalDetailOption.login)) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; @@ -979,7 +978,7 @@ function getOptions( _.every(selectedOptions, (option) => option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number.input)))) && - !_.find(loginOptionsToExclude, (loginOptionToExclude) => loginOptionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + !_.find(optionsToExclude, (optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers ) { @@ -1322,8 +1321,8 @@ export { getIOUConfirmationOptionsFromParticipants, getSearchText, getAllReportErrors, - getPolicyExpenseReportOptions, - getParticipantsOptions, + getPolicyExpenseReportOption, + getParticipantsOption, isSearchStringMatch, shouldOptionShowTooltip, getLastMessageTextForReport, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ae44445e1693..02b5d7017c48 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -676,9 +676,10 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, existingSplitChatReportID = '') { const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = _.map(participants, (participant) => Number(participant.accountID)); - const existingSplitChatReport = existingSplitChatReportID - ? allReports[`${ONYXKEYS.COLLECTION.REPORT}${existingSplitChatReportID}`] - : ReportUtils.getChatByParticipants(participantAccountIDs); + const existingSplitChatReport = + existingSplitChatReportID || participants[0].reportID + ? allReports[`${ONYXKEYS.COLLECTION.REPORT}${existingSplitChatReportID || participants[0].reportID}`] + : ReportUtils.getChatByParticipants(participantAccountIDs); const splitChatReport = existingSplitChatReport || ReportUtils.buildOptimisticChatReport(participantAccountIDs); const isOwnPolicyExpenseChat = splitChatReport.isOwnPolicyExpenseChat; @@ -1023,6 +1024,7 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou transactionID: splitData.transactionID, reportActionID: splitData.reportActionID, createdReportActionID: splitData.createdReportActionID, + policyID: splitData.policyID, }, onyxData, ); diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js index b11b16fa3c1b..0107b041abf4 100644 --- a/src/pages/iou/SplitBillDetailsPage.js +++ b/src/pages/iou/SplitBillDetailsPage.js @@ -61,14 +61,11 @@ function SplitBillDetailsPage(props) { let participants; if (ReportUtils.isPolicyExpenseChat(props.report)) { participants = [ - ...OptionsListUtils.getParticipantsOptions([{accountID: participantAccountIDs[0], selected: true}], props.personalDetails), - ...OptionsListUtils.getPolicyExpenseReportOptions({...props.report, selected: true}), + OptionsListUtils.getParticipantsOption({accountID: participantAccountIDs[0], selected: true}, props.personalDetails), + OptionsListUtils.getPolicyExpenseReportOption({...props.report, selected: true}), ]; } else { - participants = OptionsListUtils.getParticipantsOptions( - _.map(participantAccountIDs, (accountID) => ({accountID, selected: true})), - props.personalDetails, - ); + participants = _.map(participantAccountIDs, (accountID) => OptionsListUtils.getParticipantsOption({accountID, selected: true}, props.personalDetails)); } const payeePersonalDetails = props.personalDetails[reportAction.actorAccountID]; const participantsExcludingPayee = _.filter(participants, (participant) => participant.accountID !== reportAction.actorAccountID); diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 4216ff7f2f8b..224f661915d8 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -68,9 +68,10 @@ function MoneyRequestConfirmPage(props) { const reportID = useRef(lodashGet(props.route, 'params.reportID', '')); const participants = useMemo( () => - lodashGet(props.iou.participants, [0, 'isPolicyExpenseChat'], false) - ? OptionsListUtils.getPolicyExpenseReportOptions(props.iou.participants[0]) - : OptionsListUtils.getParticipantsOptions(props.iou.participants, props.personalDetails), + _.map(props.iou.participants, (participant) => { + const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, props.personalDetails); + }), [props.iou.participants, props.personalDetails], ); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 1d9f12a9cdbb..cb9f4cdd9b7f 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -50,6 +50,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { const iouType = useRef(lodashGet(route, 'params.iouType', '')); const reportID = useRef(lodashGet(route, 'params.reportID', '')); const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, selectedTab); + const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT; const [headerTitle, setHeaderTitle] = useState(); @@ -117,6 +118,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) { safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} iouType={iouType.current} isDistanceRequest={isDistanceRequest} + isScanRequest={isScanRequest} /> )} diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 9ff787ebe21b..de223823c8f4 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -3,6 +3,7 @@ import {View} from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import lodashGet from 'lodash/get'; import ONYXKEYS from '../../../../ONYXKEYS'; import styles from '../../../../styles/styles'; import OptionsSelector from '../../../../components/OptionsSelector'; @@ -58,6 +59,9 @@ const propTypes = { /** Whether the money request is a distance request or not */ isDistanceRequest: PropTypes.bool, + /** Whether the money request is a scan request or not */ + isScanRequest: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -69,6 +73,7 @@ const defaultProps = { reports: {}, betas: [], isDistanceRequest: false, + isScanRequest: false, }; function MoneyRequestParticipantsSelector({ @@ -84,6 +89,7 @@ function MoneyRequestParticipantsSelector({ safeAreaPaddingBottomStyle, iouType, isDistanceRequest, + isScanRequest, }) { const [searchTerm, setSearchTerm] = useState(''); const [newChatOptions, setNewChatOptions] = useState({ @@ -105,7 +111,10 @@ function MoneyRequestParticipantsSelector({ newSections.push({ title: undefined, - data: OptionsListUtils.getParticipantsOptions(participants, personalDetails), + data: _.map(participants, (participant) => { + const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + }), shouldShow: true, indexOffset, }); @@ -159,14 +168,27 @@ function MoneyRequestParticipantsSelector({ */ const addParticipantToSelection = useCallback( (option) => { - const isOptionInList = _.some(participants, (selectedOption) => selectedOption.accountID === option.accountID); - + const isOptionSelected = (selectedOption) => { + if (selectedOption.accountID && selectedOption.accountID === option.accountID) { + return true; + } + + if (selectedOption.reportID && selectedOption.reportID === option.reportID) { + return true; + } + + return false; + }; + const isOptionInList = _.some(participants, isOptionSelected); let newSelectedOptions; if (isOptionInList) { - newSelectedOptions = _.reject(participants, (selectedOption) => selectedOption.accountID === option.accountID); + newSelectedOptions = _.reject(participants, isOptionSelected); } else { - newSelectedOptions = [...participants, {accountID: option.accountID, login: option.login, selected: true}]; + newSelectedOptions = [ + ...participants, + {accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true}, + ]; } onAddParticipants(newSelectedOptions); @@ -227,10 +249,17 @@ function MoneyRequestParticipantsSelector({ }); }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest]); + // Right now you can't split a request with a workspace and other additional participants + // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent + // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants + const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); + const shouldShowConfirmButton = !(participants.length > 1 && hasPolicyExpenseChatParticipant); + const isAllowedToSplit = !isDistanceRequest && !isScanRequest; + return ( 0 ? safeAreaPaddingBottomStyle : {}]}>