diff --git a/src/CONST.js b/src/CONST.js index 46aa9a1943e9..af25f43d47c0 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -494,6 +494,7 @@ const CONST = { TASKEDITED: 'TASKEDITED', TASKCANCELLED: 'TASKCANCELLED', IOU: 'IOU', + REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', RENAMED: 'RENAMED', CHRONOSOOOLIST: 'CHRONOSOOOLIST', TASKCOMPLETED: 'TASKCOMPLETED', diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 4739e9ed1f12..ff97c9be24a6 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -180,7 +180,7 @@ class AddPlaidBankAccount extends React.Component { token={token} onSuccess={({publicToken, metadata}) => { Log.info('[PlaidLink] Success!'); - BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit); + BankAccounts.openPlaidBankAccountSelector(publicToken, metadata.institution.name, this.props.allowDebit, this.props.bankAccountID); }} onError={(error) => { Log.hmmm('[PlaidLink] Error: ', error.message); diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index f8c45b585531..3030e2227fad 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -20,6 +20,7 @@ import PressableWithSecondaryInteraction from '../PressableWithSecondaryInteract import * as ReportActionContextMenu from '../../pages/home/report/ContextMenu/ReportActionContextMenu'; import * as ContextMenuActions from '../../pages/home/report/ContextMenu/ContextMenuActions'; import * as OptionsListUtils from '../../libs/OptionsListUtils'; +import * as ReportUtils from '../../libs/ReportUtils'; import useLocalize from '../../hooks/useLocalize'; const propTypes = { @@ -90,7 +91,7 @@ function OptionRowLHN(props) { const shouldShowGreenDotIndicator = !hasBrickError && (optionItem.isUnreadWithMention || - (optionItem.hasOutstandingIOU && !optionItem.isIOUReportOwner) || + ReportUtils.isWaitingForIOUActionFromCurrentUser(optionItem) || (optionItem.isTaskReport && optionItem.isTaskAssignee && !optionItem.isCompletedTaskReport && !optionItem.isArchivedRoom)); /** diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 770b778247f3..8ec40eb14996 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -59,7 +59,7 @@ function MoneyReportHeader(props) { const policy = props.policies[`${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}`]; const isPayer = Policy.isAdminOfFreePolicy([policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID); - const shouldShowSettlementButton = !isSettled && isPayer; + const shouldShowSettlementButton = !isSettled && isPayer && !moneyRequestReport.isWaitingOnBankAccount; const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'payPalMeAddress'])); const formattedAmount = CurrencyUtils.convertToDisplayString(ReportUtils.getMoneyRequestTotal(props.report), props.report.currency); diff --git a/src/components/MoneyRequestDetails.js b/src/components/MoneyRequestDetails.js index f8c371aa5a58..5bb7c029d44b 100644 --- a/src/components/MoneyRequestDetails.js +++ b/src/components/MoneyRequestDetails.js @@ -89,9 +89,16 @@ function MoneyRequestDetails(props) { : UserUtils.getAvatar(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'avatar']), moneyRequestReport.managerID); const isPayer = Policy.isAdminOfFreePolicy([props.policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID); - const shouldShowSettlementButton = !isSettled && !props.isSingleTransactionView && isPayer; + const shouldShowSettlementButton = moneyRequestReport.reportID && !isSettled && !props.isSingleTransactionView && isPayer && !moneyRequestReport.isWaitingOnBankAccount; const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.ownerAccountID, 'payPalMeAddress'])); + let description = `${props.translate('iou.amount')} • ${props.translate('iou.cash')}`; + if (isSettled) { + description += ` • ${props.translate('iou.settledExpensify')}`; + } else if (props.report.isWaitingOnBankAccount) { + description += ` • ${props.translate('iou.pending')}`; + } + const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(props.report); return ( {CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} - {!props.iouReport.hasOutstandingIOU && !props.isBillSplit && ( + {ReportUtils.isSettled(props.iouReport.reportID) && !props.isBillSplit && ( - - {props.translate(ReportUtils.isSettled(props.iouReportID) ? 'iou.payerPaid' : 'iou.payerOwes', {payer: managerName})} - + {previewMessage} @@ -128,7 +146,7 @@ function ReportPreview(props) { )} - {isCurrentUserManager && !ReportUtils.isSettled(props.iouReport.reportID) && ( + {!_.isEmpty(props.iouReport) && isCurrentUserManager && !ReportUtils.isSettled(props.iouReportID) && !props.iouReport.isWaitingOnBankAccount && ( `${payer} paid ${amount}`, payerPaid: ({payer}) => `${payer} paid: `, payerSettled: ({amount}) => `paid ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, paidElsewhereWithAmount: ({amount}) => `paid ${amount} elsewhere`, paidUsingPaypalWithAmount: ({amount}) => `paid ${amount} using Paypal.me`, paidUsingExpensifyWithAmount: ({amount}) => `paid ${amount} using Expensify`, diff --git a/src/languages/es.js b/src/languages/es.js index 5eea74099e4c..b7b243efda98 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -350,6 +350,7 @@ export default { sendMoney: 'Enviar dinero', pay: 'Pagar', viewDetails: 'Ver detalles', + pending: 'Pendiente', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settledPaypalMe: 'Pagado con PayPal.me', @@ -364,6 +365,8 @@ export default { payerPaidAmount: ({payer, amount}) => `${payer} pagó ${amount}`, payerPaid: ({payer}) => `${payer} pagó: `, payerSettled: ({amount}) => `pagó ${amount}`, + waitingOnBankAccount: ({submitterDisplayName}) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} agregue una Cuenta bancaria`, + settledAfterAddedBankAccount: ({submitterDisplayName, amount}) => `${submitterDisplayName} agregó una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({amount}) => `pagó ${amount} de otra forma`, paidUsingPaypalWithAmount: ({amount}) => `pagó ${amount} con PayPal.me`, paidUsingExpensifyWithAmount: ({amount}) => `pagó ${amount} con Expensify`, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2acb1f51cbe7..54e34407cfb1 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -64,26 +64,6 @@ Onyx.connect({ }, }); -const expenseReports = {}; -const iouReports = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - callback: (report, key) => { - if (!report || !key || !_.isNumber(report.ownerAccountID)) { - return; - } - - if (ReportUtils.isExpenseReport(report)) { - expenseReports[key] = report; - return; - } - - if (ReportUtils.isIOUReport(report)) { - iouReports[key] = report; - } - }, -}); - const lastReportActions = {}; const allSortedReportActions = {}; Onyx.connect({ @@ -455,6 +435,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { isDefaultRoom: false, isPinned: false, hasOutstandingIOU: false, + isWaitingOnBankAccount: false, iouReportID: null, isIOUReportOwner: null, iouReportAmount: 0, @@ -496,6 +477,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.keyForList = String(report.reportID); result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); result.hasOutstandingIOU = report.hasOutstandingIOU; + result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -531,8 +513,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.alternateText = LocalePhoneNumber.formatPhoneNumber(lodashGet(personalDetails, [accountIDs[0], 'login'], '')); } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, iouReports); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result, iouReports); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); + result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); if (!hasMultipleParticipants) { result.login = personalDetail.login; @@ -652,7 +634,7 @@ function getOptions( const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId(), false, iouReports, betas, policies)); + const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId(), false, null, betas, policies)); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) @@ -695,6 +677,11 @@ function getOptions( return; } + // In case user needs to add credit bank account, don't allow them to request more money from the workspace. + if (includeOwnedWorkspaceChats && ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(report)) { + return; + } + // Save the report in the map if this is a single participant so we can associate the reportID with the // personal detail option later. Individuals should not be associated with single participant // policyExpenseChats or chatRooms since those are not people. diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c0ada6d34868..222baab714f4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -235,7 +235,19 @@ function canEditReportAction(reportAction) { * @returns {Boolean} */ function isSettled(reportID) { - return !lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'hasOutstandingIOU']); + const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); + return !_.isEmpty(report) && !report.hasOutstandingIOU && !report.isWaitingOnBankAccount; +} + +/** + * Whether the current user is the submitter of the report + * + * @param {String} reportID + * @returns {Boolean} + */ +function isCurrentUserSubmitter(reportID) { + const report = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}); + return report && report.ownerEmail === currentUserEmail; } /** @@ -1010,15 +1022,54 @@ function getMoneyRequestAction(reportAction = {}) { return {amount, currency, comment}; } +/** + * Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account) + * + * @param {Object} report (chatReport or iouReport) + * @param {Object} allReportsDict + * @returns {boolean} + */ +function isWaitingForIOUActionFromCurrentUser(report, allReportsDict = null) { + const allAvailableReports = allReportsDict || allReports; + if (!report || !allAvailableReports) { + return false; + } + + // Money request waiting for current user to add their credit bank account + if (report.ownerAccountID === currentUserAccountID && report.isWaitingOnBankAccount) { + return true; + } + + let reportToLook = report; + if (report.iouReportID) { + const iouReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + if (iouReport) { + reportToLook = iouReport; + } + } + // Money request waiting for current user to Pay (from chat or from iou report) + if (reportToLook.ownerAccountID && (reportToLook.ownerAccountID !== currentUserAccountID || currentUserAccountID === reportToLook.managerID) && reportToLook.hasOutstandingIOU) { + return true; + } + + return false; +} + /** * @param {Object} report - * @param {String} report.iouReportID - * @param {Object} moneyRequestReports + * @param {Object} allReportsDict * @returns {Number} */ -function getMoneyRequestTotal(report, moneyRequestReports = {}) { - if (report.hasOutstandingIOU || isMoneyRequestReport(report)) { - const moneyRequestReport = moneyRequestReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`] || report; +function getMoneyRequestTotal(report, allReportsDict = null) { + const allAvailableReports = allReportsDict || allReports; + let moneyRequestReport; + if (isMoneyRequestReport(report)) { + moneyRequestReport = report; + } + if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + } + if (moneyRequestReport) { const total = lodashGet(moneyRequestReport, 'total', 0); if (total !== 0) { @@ -1072,8 +1123,17 @@ function getPolicyExpenseChatName(report, policy = undefined) { function getMoneyRequestReportName(report, policy = undefined) { const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID); + const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', {payer: payerName, amount: formattedAmount}); + + if (report.isWaitingOnBankAccount) { + return `${payerPaidAmountMesssage} • ${Localize.translateLocal('iou.pending')}`; + } - return Localize.translateLocal(report.hasOutstandingIOU ? 'iou.payerOwesAmount' : 'iou.payerPaidAmount', {payer: payerName, amount: formattedAmount}); + if (report.hasOutstandingIOU) { + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); + } + + return payerPaidAmountMesssage; } /** @@ -1119,7 +1179,15 @@ function getReportPreviewMessage(report, reportAction = {}) { } return Localize.translateLocal(translatePhraseKey, {amount: formattedAmount}); } - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); + + if (report.hasOutstandingIOU) { + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount}); + } + + if (report.isWaitingOnBankAccount) { + const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID, true); + return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); + } } /** @@ -2007,44 +2075,25 @@ function isUnreadWithMention(report) { } /** - * Determines if a report has an outstanding IOU that doesn't belong to the currently logged in user - * * @param {Object} report - * @param {String} report.iouReportID - * @param {Object} iouReports - * @returns {boolean} + * @param {Object} allReportsDict + * @returns {Boolean} */ -function hasOutstandingIOU(report, iouReports) { - if (!report || !report.iouReportID || _.isUndefined(report.hasOutstandingIOU)) { - return false; - } - - const iouReport = iouReports && iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; - if (!iouReport || !iouReport.ownerAccountID) { - return false; - } - - if (iouReport.ownerAccountID === currentUserAccountID) { +function isIOUOwnedByCurrentUser(report, allReportsDict = null) { + const allAvailableReports = allReportsDict || allReports; + if (!report || !allAvailableReports) { return false; } - return report.hasOutstandingIOU; -} - -/** - * @param {Object} report - * @param {String} report.iouReportID - * @param {Object} iouReports - * @returns {Boolean} - */ -function isIOUOwnedByCurrentUser(report, iouReports = {}) { - if (report.hasOutstandingIOU) { - const iouReport = iouReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + let reportToLook = report; + if (report.iouReportID) { + const iouReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; if (iouReport) { - return iouReport.ownerAccountID === currentUserAccountID; + reportToLook = iouReport; } } - return false; + + return reportToLook.ownerAccountID === currentUserAccountID; } /** @@ -2149,7 +2198,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep // Include reports if they have a draft, are pinned, or have an outstanding IOU // These are always relevant to the user no matter what view mode the user prefers - if (report.hasDraft || report.isPinned || hasOutstandingIOU(report, iouReports)) { + if (report.hasDraft || report.isPinned || isWaitingForIOUActionFromCurrentUser(report, iouReports)) { return true; } @@ -2381,6 +2430,23 @@ function getReportIDFromLink(url) { return reportID; } +/** + * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. + * + * @param {Object} chatReport + * @returns {Boolean} + */ +function hasIOUWaitingOnCurrentUserBankAccount(chatReport) { + if (chatReport.iouReportID) { + const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; + if (iouReport && iouReport.isWaitingOnBankAccount && iouReport.ownerAccountID === currentUserAccountID) { + return true; + } + } + + return false; +} + /** * Users can request money in policy expense chats only if they are in a role of a member in the chat (in other words, if it's their policy expense chat) * @@ -2388,6 +2454,10 @@ function getReportIDFromLink(url) { * @returns {Boolean} */ function canRequestMoney(report) { + // Prevent requesting money if pending iou waiting for their bank account already exists. + if (hasIOUWaitingOnCurrentUserBankAccount(report)) { + return false; + } return !isPolicyExpenseChat(report) || report.isOwnPolicyExpenseChat; } @@ -2648,7 +2718,7 @@ export { isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyAccountIDs, hasExpensifyGuidesEmails, - hasOutstandingIOU, + isWaitingForIOUActionFromCurrentUser, isIOUOwnedByCurrentUser, getMoneyRequestTotal, canShowReportRecipientLocalTime, @@ -2691,6 +2761,7 @@ export { getIOUReportActionMessage, getDisplayNameForParticipant, isChatReport, + isCurrentUserSubmitter, isExpenseReport, isExpenseRequest, isIOUReport, @@ -2707,6 +2778,7 @@ export { getCommentLength, getParsedComment, getMoneyRequestOptions, + hasIOUWaitingOnCurrentUserBankAccount, canRequestMoney, getWhisperDisplayNames, getWorkspaceAvatar, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 757ff91834f2..dfb1637419cb 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -14,24 +14,6 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; -// Note: Earlier SidebarUtils.getOrderedReportIDs() used to have no parameters. All the needed data was loaded here directly -// using Onyx.connect. We then had to connect SidebarLinks additionally to all the keys that were used in SidebarUtils.getOrderedReportIDs(). -// That's because we wanted to cause a re-render in SidebarLinks to run SidebarUtils.getOrderedReportIDs() again. -// This caused bugs in the past as we were forgetting to include e.g. very nested data. -// Now we pass all the data from SidebarLinks props to SidebarUtils.getOrderedReportIDs(). -// This makes the code easier to understand and less error prone. - -// However, in getOptionData() we still use Onyx.connect() to get some of the data. -// That's because we can't connect to specific nested data. Once we added dependent -// selectors to Onyx, we can remove this as well. - -let allReports; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (val) => (allReports = val), -}); - const visibleReportActionItems = {}; const lastReportActions = {}; const reportActions = {}; @@ -110,6 +92,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p const reportsToDisplay = _.filter(allReportsDict, (report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, allReportsDict, betas, policies, allReportActions), ); + if (_.isEmpty(reportsToDisplay)) { // Display Concierge chat report when there is no report to be displayed const conciergeChatReport = _.find(allReportsDict, ReportUtils.isConciergeChatReport); @@ -145,14 +128,13 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p let draftReports = []; let nonArchivedReports = []; let archivedReports = []; - _.each(reportsToDisplay, (report) => { if (report.isPinned) { pinnedReports.push(report); return; } - if (report.hasOutstandingIOU && !ReportUtils.isIOUOwnedByCurrentUser(report, allReportsDict)) { + if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report, allReportsDict)) { outstandingIOUReports.push(report); return; } @@ -223,6 +205,7 @@ function getOptionData(report, personalDetails, preferredLocale, policy) { participantsList: null, login: null, accountID: null, + managerID: null, reportID: null, phoneNumber: null, payPalMeAddress: null, @@ -242,6 +225,7 @@ function getOptionData(report, personalDetails, preferredLocale, policy) { isPolicyExpenseChat: false, isMoneyRequestReport: false, isExpenseRequest: false, + isWaitingOnBankAccount: false, isLastMessageDeletedParentAction: false, }; @@ -264,6 +248,7 @@ function getOptionData(report, personalDetails, preferredLocale, policy) { result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions); result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; + result.managerID = report.managerID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); result.isUnreadWithMention = ReportUtils.isUnreadWithMention(report); @@ -274,6 +259,8 @@ function getOptionData(report, personalDetails, preferredLocale, policy) { result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.parentReportID = report.parentReportID || null; + result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; + result.notificationPreference = report.notificationPreference || null; const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; const subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -347,8 +334,8 @@ function getOptionData(report, personalDetails, preferredLocale, policy) { result.alternateText = lastMessageText || formattedLogin; } - result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result, allReports); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result, allReports); + result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); + result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 396cb83aee58..43e00c104f48 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -33,8 +33,17 @@ function openPlaidView() { clearPlaid().then(() => ReimbursementAccount.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID)); } -function openPersonalBankAccountSetupView() { - clearPlaid().then(() => Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT)); +/** + * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished. + * @param {String} exitReportID + */ +function openPersonalBankAccountSetupView(exitReportID) { + clearPlaid().then(() => { + if (exitReportID) { + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID}); + } + Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT); + }); } function clearPersonalBankAccount() { diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index e480120a7323..dbea53c54c3a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -330,17 +330,17 @@ function requestMoney(report, amount, currency, payeeEmail, payeeAccountID, part } // STEP 2: Get existing IOU report and update its total OR build a new optimistic one - const isNewIOUReport = !chatReport.iouReportID; - let iouReport; + const isNewIOUReport = !chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport); + let iouReport = isNewIOUReport ? null : iouReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; - if (!isNewIOUReport) { + if (iouReport) { if (isPolicyExpenseChat) { - iouReport = {...iouReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]}; + iouReport = {...iouReport}; // Because of the Expense reports are stored as negative values, we substract the total from the amount iouReport.total -= amount; } else { - iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`], payeeAccountID, amount, currency); + iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency); } } else { iouReport = isPolicyExpenseChat diff --git a/src/libs/actions/Plaid.js b/src/libs/actions/Plaid.js index 3155058624af..53763980d285 100644 --- a/src/libs/actions/Plaid.js +++ b/src/libs/actions/Plaid.js @@ -40,14 +40,16 @@ function openPlaidBankLogin(allowDebit, bankAccountID) { * @param {String} publicToken * @param {String} bankName * @param {Boolean} allowDebit + * @param {Number} bankAccountID */ -function openPlaidBankAccountSelector(publicToken, bankName, allowDebit) { +function openPlaidBankAccountSelector(publicToken, bankName, allowDebit, bankAccountID) { API.read( 'OpenPlaidBankAccountSelector', { publicToken, allowDebit, bank: bankName, + bankAccountID, }, { optimisticData: [ diff --git a/src/libs/actions/ReimbursementAccount/store.js b/src/libs/actions/ReimbursementAccount/store.js index d1e41ba7f8ec..422c0ffc43dd 100644 --- a/src/libs/actions/ReimbursementAccount/store.js +++ b/src/libs/actions/ReimbursementAccount/store.js @@ -1,6 +1,8 @@ import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; +import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; +import BankAccount from '../../models/BankAccount'; /** Reimbursement account actively being set up */ let reimbursementAccountInSetup = {}; @@ -43,6 +45,13 @@ function getBankAccountList() { return bankAccountList; } +function hasCreditBankAccount() { + return _.some(bankAccountList, (bankAccountJSON) => { + const bankAccount = new BankAccount(bankAccountJSON); + return bankAccount.isDefaultCredit(); + }); +} + function getCredentials() { return credentials; } @@ -51,4 +60,4 @@ function getReimbursementAccountWorkspaceID() { return reimbursementAccountWorkspaceID; } -export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID}; +export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index b23f706d4481..96140b33928f 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -32,6 +32,9 @@ const propTypes = { /** Whether we should show the view that the bank account was successfully added */ shouldShowSuccess: PropTypes.bool, + /** Any reportID we should redirect to at the end of the flow */ + exitReportID: PropTypes.string, + /** Whether the form is loading */ isLoading: PropTypes.bool, @@ -47,6 +50,7 @@ const defaultProps = { shouldShowSuccess: false, isLoading: false, plaidAccountID: '', + exitReportID: '', }, }; @@ -56,6 +60,7 @@ class AddPersonalBankAccountPage extends React.Component { this.validate = this.validate.bind(this); this.submit = this.submit.bind(this); + this.exitFlow = this.exitFlow.bind(this); this.state = { selectedPlaidAccountID: '', @@ -81,6 +86,15 @@ class AddPersonalBankAccountPage extends React.Component { BankAccounts.addPersonalBankAccount(selectedPlaidBankAccount); } + exitFlow() { + const exitReportID = lodashGet(this.props, 'personalBankAccount.exitReportID'); + if (exitReportID) { + Navigation.dismissModal(exitReportID); + } else { + Navigation.goBack(ROUTES.SETTINGS_PAYMENTS); + } + } + render() { const shouldShowSuccess = lodashGet(this.props, 'personalBankAccount.shouldShowSuccess', false); @@ -92,7 +106,7 @@ class AddPersonalBankAccountPage extends React.Component { > Navigation.goBack(ROUTES.SETTINGS_PAYMENTS)} + onBackButtonPress={this.exitFlow} /> {shouldShowSuccess ? ( { - Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); - }} + onButtonPress={this.exitFlow} /> ) : (
); + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + const shouldShowAddCreditBankAccountButton = + ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !store.hasCreditBankAccount() && !ReportUtils.isSettled(props.report.reportID); + + children = ( + + {shouldShowAddCreditBankAccountButton ? ( +