diff --git a/android/app/build.gradle b/android/app/build.gradle index 56a987269f6e..d8d0c11a1a0c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001035409 - versionName "1.3.54-9" + versionCode 1001035411 + versionName "1.3.54-11" } flavorDimensions "default" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3eb22374f955..92c61cb81b2c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -203,6 +203,7 @@ platform :ios do build_app( workspace: "./ios/NewExpensify.xcworkspace", scheme: "New Expensify", + output_name: "New Expensify.ipa", export_options: { manageAppVersionAndBuildNumber: false } diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 844086950b32..8817f8560fed 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -32,7 +32,7 @@ CFBundleVersion - 1.3.54.9 + 1.3.54.11 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2b3e076f1db8..31dab672cd4e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.3.54.9 + 1.3.54.11 diff --git a/package-lock.json b/package-lock.json index 7951d7dc2c8f..3e886a89af2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.54-9", + "version": "1.3.54-11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.54-9", + "version": "1.3.54-11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1681ddfb597b..040b9a78896a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.54-9", + "version": "1.3.54-11", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.js b/src/CONST.js index a0c6cbf2bcf3..1644d40ad249 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -2537,6 +2537,17 @@ const CONST = { DISTANCE: 'distance', }, STATUS_TEXT_MAX_LENGTH: 100, + NAVIGATION: { + TYPE: { + FORCED_UP: 'FORCED_UP', + UP: 'UP', + }, + ACTION_TYPE: { + REPLACE: 'REPLACE', + PUSH: 'PUSH', + NAVIGATE: 'NAVIGATE', + }, + }, }; export default CONST; diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 64b3b960581f..836e47b2ccaf 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -118,6 +118,7 @@ export default { DOWNLOAD: 'download_', POLICY: 'policy_', POLICY_MEMBERS: 'policyMembers_', + POLICY_CATEGORIES: 'policyCategories_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', REPORT: 'report_', REPORT_ACTIONS: 'reportActions_', diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 92aa99df24d6..c05cf14f2fc1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -82,6 +82,12 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, polic description += ` • ${translate('iou.pending')}`; } + // A temporary solution to hide the transaction detail + // This will be removed after we properly add the transaction as a prop + if (ReportActionsUtils.isDeletedAction(parentReportAction)) { + return null; + } + return ( diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 41f66967cc00..39f722c6b48a 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -12,6 +12,7 @@ import NAVIGATORS from '../../NAVIGATORS'; import originalGetTopmostReportId from './getTopmostReportId'; import getStateFromPath from './getStateFromPath'; import SCREENS from '../../SCREENS'; +import CONST from '../../CONST'; let resolveNavigationIsReadyPromise; const navigationIsReadyPromise = new Promise((resolve) => { @@ -127,7 +128,7 @@ function goBack(fallbackRoute = ROUTES.HOME, shouldEnforceFallback = false, shou } if (shouldEnforceFallback || (isFirstRouteInNavigator && fallbackRoute)) { - navigate(fallbackRoute, 'UP'); + navigate(fallbackRoute, CONST.NAVIGATION.TYPE.UP); return; } diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.js index c610ae710992..884a8aa02190 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.js @@ -4,6 +4,7 @@ import NAVIGATORS from '../../NAVIGATORS'; import linkingConfig from './linkingConfig'; import getTopmostReportId from './getTopmostReportId'; import getStateFromPath from './getStateFromPath'; +import CONST from '../../CONST'; /** * Motivation for this function is described in NAVIGATION.md @@ -59,19 +60,23 @@ export default function linkTo(navigation, path, type) { const action = getActionFromState(state, linkingConfig.config); // If action type is different than NAVIGATE we can't change it to the PUSH safely - if (action.type === 'NAVIGATE') { - // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH - if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) { - action.type = 'PUSH'; + if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { + // In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack + if (type === CONST.NAVIGATION.TYPE.FORCED_UP) { + action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; + + // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack + } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) { + action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow // and at the same time we want the back button to go to the page we were before the deeplink - } else if (type === 'UP') { - action.type = 'REPLACE'; + } else if (type === CONST.NAVIGATION.TYPE.UP) { + action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; // If this action is navigating to the RightModalNavigator and the last route on the root navigator is not RightModalNavigator then push } else if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && _.last(root.getState().routes).name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { - action.type = 'PUSH'; + action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; } } diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 60a9a34838b1..e9fb92a7b020 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -640,7 +640,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, null, betas, policies)); + const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId(), false, betas, policies)); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.js index 582271d6610e..164f284a4ef5 100644 --- a/src/libs/PolicyUtils.js +++ b/src/libs/PolicyUtils.js @@ -10,7 +10,7 @@ import ONYXKEYS from '../ONYXKEYS'; * @returns {Array} */ function getActivePolicies(policies) { - return _.filter(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + return _.filter(policies, (policy) => policy && policy.isPolicyExpenseChatEnabled && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); } /** @@ -86,7 +86,7 @@ function getPolicyBrickRoadIndicatorStatus(policy, policyMembersCollection) { function shouldShowPolicy(policy, isOffline) { return ( policy && - policy.type === CONST.POLICY.TYPE.FREE && + policy.isPolicyExpenseChatEnabled && policy.role === CONST.POLICY.ROLE.ADMIN && (isOffline || policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policy.errors)) ); diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index e5fe1437512e..f68cfe6adeba 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -566,6 +566,14 @@ function isMessageDeleted(reportAction) { return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false); } +/** + * @param {*} reportAction + * @returns {Boolean} + */ +function isSplitBillAction(reportAction) { + return lodashGet(reportAction, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; +} + export { getSortedReportActions, getLastVisibleAction, @@ -599,4 +607,5 @@ export { isWhisperAction, isPendingRemove, getReportAction, + isSplitBillAction, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index cc12f390dd7b..3974c339eb0f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1110,12 +1110,10 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR * 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) { +function isWaitingForIOUActionFromCurrentUser(report) { + if (!report) { return false; } @@ -1124,15 +1122,8 @@ function isWaitingForIOUActionFromCurrentUser(report, allReportsDict = null) { 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) { + // Money request waiting for current user to Pay (from expense or iou report) + if (report.hasOutstandingIOU && report.ownerAccountID && (report.ownerAccountID !== currentUserAccountID || currentUserAccountID === report.managerID)) { return true; } @@ -2470,14 +2461,13 @@ function canAccessReport(report, policies, betas, allReportActions) { * @param {Object} report * @param {String} currentReportId * @param {Boolean} isInGSDMode - * @param {Object} iouReports * @param {String[]} betas * @param {Object} policies * @param {Object} allReportActions * @param {Boolean} excludeEmptyChats * @returns {boolean} */ -function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouReports, betas, policies, allReportActions, excludeEmptyChats = false) { +function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, excludeEmptyChats = false) { const isInDefaultMode = !isInGSDMode; // Exclude reports that have no data because there wouldn't be anything to show in the option item. @@ -2504,7 +2494,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, iouRep } // Include reports that are relevant to the user in any view mode. Criteria include having a draft, having an outstanding IOU, or being assigned to an open task. - if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report, iouReports) || isWaitingForTaskCompleteFromAssignee(report)) { + if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 8afe05650bc6..c384d3c17a39 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -86,9 +86,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p const isInDefaultMode = !isInGSDMode; // Filter out all the reports that shouldn't be displayed - const reportsToDisplay = _.filter(allReportsDict, (report) => - ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, allReportsDict, betas, policies, allReportActions, true), - ); + const reportsToDisplay = _.filter(allReportsDict, (report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, policies, allReportActions, true)); if (_.isEmpty(reportsToDisplay)) { // Display Concierge chat report when there is no report to be displayed @@ -131,7 +129,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p return; } - if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report, allReportsDict)) { + if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report)) { outstandingIOUReports.push(report); return; } diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index efce0800e849..a8fd828d07d4 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -150,11 +150,11 @@ function getCurrency(transaction) { * @returns {String} */ function getCreated(transaction) { - const created = lodashGet(transaction, 'modifiedCreated', ''); + const created = lodashGet(transaction, 'modifiedCreated', '') || lodashGet(transaction, 'created', ''); if (created) { return format(new Date(created), CONST.DATE.FNS_FORMAT_STRING); } - return format(new Date(lodashGet(transaction, 'created', Date.now())), CONST.DATE.FNS_FORMAT_STRING); + return ''; } /** diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 809491c14950..46cc71850ccb 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -66,12 +66,6 @@ Onyx.connect({ callback: (val) => (allPersonalDetails = val), }); -let loginList; -Onyx.connect({ - key: ONYXKEYS.LOGIN_LIST, - callback: (val) => (loginList = val), -}); - /** * Stores in Onyx the policy ID of the last workspace that was accessed by the user * @param {String|null} policyID @@ -161,16 +155,6 @@ function isAdminOfFreePolicy(policies) { return _.some(policies, (policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); } -/** - * Is the user the owner of the given policy? - * - * @param {Object} policy - * @returns {Boolean} - */ -function isPolicyOwner(policy) { - return _.keys(loginList).includes(policy.owner); -} - /** * Check if the user has any active free policies (aka workspaces) * @@ -933,6 +917,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName name: workspaceName, role: CONST.POLICY.ROLE.ADMIN, owner: sessionEmail, + isPolicyExpenseChatEnabled: true, outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, customUnits, @@ -1200,6 +1185,5 @@ export { openWorkspaceInvitePage, removeWorkspace, setWorkspaceInviteMembersDraft, - isPolicyOwner, clearErrors, }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 533b6d0af650..9853ebdd1581 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -849,6 +849,20 @@ function handleReportChanged(report) { return; } + // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. + // In this case, the API will let us know by returning a preexistingReportID. + // We should clear out the optimistically created report and re-route the user to the preexisting report. + if (report && report.reportID && report.preexistingReportID) { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null); + + // Only re-route them if they are still looking at the optimistically created report + if (Navigation.getActiveRoute().includes(`/r/${report.reportID}`)) { + // Pass 'FORCED_UP' type to replace new report on second login with proper one in the Navigation + Navigation.navigate(ROUTES.getReportRoute(report.preexistingReportID), CONST.NAVIGATION.TYPE.FORCED_UP); + } + return; + } + if (report && report.reportID) { allReports[report.reportID] = report; @@ -1720,7 +1734,7 @@ function openReportFromDeepLink(url, isAuthenticated) { InteractionManager.runAfterInteractions(() => { SidebarUtils.isSidebarLoadedReady().then(() => { if (reportID) { - Navigation.navigate(ROUTES.getReportRoute(reportID), 'UP'); + Navigation.navigate(ROUTES.getReportRoute(reportID), CONST.NAVIGATION.TYPE.UP); } if (route === ROUTES.CONCIERGE) { navigateToConciergeChat(); diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 445f10c1812e..c5188da02195 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -1,8 +1,8 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; -import {format} from 'date-fns'; +import compose from '../libs/compose'; import CONST from '../CONST'; import Navigation from '../libs/Navigation/Navigation'; import ONYXKEYS from '../ONYXKEYS'; @@ -31,25 +31,39 @@ const propTypes = { /** The report object for the thread report */ report: reportPropTypes, + + /** The parent report object for the thread report */ + parentReport: reportPropTypes, }; const defaultProps = { report: {}, + parentReport: {}, }; -function EditRequestPage({report, route}) { - const transactionID = lodashGet(ReportActionsUtils.getParentReportAction(report), 'originalMessage.IOUTransactionID', ''); - const transaction = TransactionUtils.getTransaction(transactionID); +function EditRequestPage({report, route, parentReport}) { + const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const transaction = TransactionUtils.getLinkedTransaction(parentReportAction); const {amount: transactionAmount, currency: transactionCurrency, comment: transactionDescription} = ReportUtils.getTransactionDetails(transaction); // Take only the YYYY-MM-DD value - const transactionCreatedDate = new Date(TransactionUtils.getCreated(transaction)); - const transactionCreated = format(transactionCreatedDate, CONST.DATE.FNS_FORMAT_STRING); + const transactionCreated = TransactionUtils.getCreated(transaction); const fieldToEdit = lodashGet(route, ['params', 'field'], ''); + const isDeleted = ReportActionsUtils.isDeletedAction(parentReportAction); + const isSetted = ReportUtils.isSettled(parentReport.reportID); + + // Dismiss the modal when the request is paid or deleted + useEffect(() => { + if (!isDeleted && !isSetted) { + return; + } + Navigation.dismissModal(); + }, [isDeleted, isSetted]); + // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { - IOU.editMoneyRequest(transactionID, report.reportID, transactionChanges); + IOU.editMoneyRequest(transaction.transactionID, report.reportID, transactionChanges); Navigation.dismissModal(); } @@ -114,8 +128,15 @@ function EditRequestPage({report, route}) { EditRequestPage.displayName = 'EditRequestPage'; EditRequestPage.propTypes = propTypes; EditRequestPage.defaultProps = defaultProps; -export default withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`, - }, -})(EditRequestPage); +export default compose( + withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`, + }, + }), + withOnyx({ + parentReport: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`, + }, + }), +)(EditRequestPage); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index fafcb2c94f8c..a83ab4a42080 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -32,7 +32,7 @@ import * as ReimbursementAccountProps from './reimbursementAccountPropTypes'; import reimbursementAccountDraftPropTypes from './ReimbursementAccountDraftPropTypes'; import withPolicy from '../workspace/withPolicy'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; -import * as Policy from '../../libs/actions/Policy'; +import * as PolicyUtils from '../../libs/PolicyUtils'; const propTypes = { /** Plaid SDK token to use to initialize the widget */ @@ -332,7 +332,7 @@ class ReimbursementAccountPage extends React.Component { const policyName = lodashGet(this.props.policy, 'name'); const policyID = lodashGet(this.props.route.params, 'policyID'); - if (_.isEmpty(this.props.policy) || !Policy.isPolicyOwner(this.props.policy)) { + if (_.isEmpty(this.props.policy) || !PolicyUtils.isPolicyAdmin(this.props.policy)) { return ( - type === CONTEXT_MENU_TYPES.REPORT_ACTION && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID), + shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + if (type !== CONTEXT_MENU_TYPES.REPORT_ACTION) { + return false; + } + const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionUtils.isSplitBillAction(reportAction); + return isCommentAction || isReportPreviewAction || isIOUAction; + }, onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { hideContextMenu(false, () => { diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/index.js b/src/pages/home/report/ReactionList/PopoverReactionList/index.js index c39eeddb7fd0..327885249843 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/index.js +++ b/src/pages/home/report/ReactionList/PopoverReactionList/index.js @@ -29,7 +29,19 @@ function PopoverReactionList(props) { innerReactionListRef.current.showReactionList(event, reactionListAnchor); }; - useImperativeHandle(props.innerRef, () => ({showReactionList}), []); + const hideReactionList = () => { + innerReactionListRef.current.hideReactionList(); + }; + + /** + * Whether PopoverReactionList is active for the Report Action. + * + * @param {Number|String} actionID + * @return {Boolean} + */ + const isActiveReportAction = (actionID) => Boolean(actionID) && reactionListReportActionID === actionID; + + useImperativeHandle(props.innerRef, () => ({showReactionList, hideReactionList, isActiveReportAction})); return ( () => { - // ReportActionContextMenu and EmojiPicker are global component, - // we use showContextMenu and showEmojiPicker to show them, - // so we should also hide them when the current component is destroyed + // ReportActionContextMenu, EmojiPicker and PopoverReactionList are global components, + // we should also hide them when the current component is destroyed if (ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)) { ReportActionContextMenu.hideContextMenu(); ReportActionContextMenu.hideDeleteModal(); @@ -142,8 +143,11 @@ function ReportActionItem(props) { if (EmojiPickerAction.isActiveReportAction(props.action.reportActionID)) { EmojiPickerAction.hideEmojiPicker(true); } + if (reactionListRef.current && reactionListRef.current.isActiveReportAction(props.action.reportActionID)) { + reactionListRef.current.hideReactionList(); + } }, - [props.action.reportActionID], + [props.action.reportActionID, reactionListRef], ); const isDraftEmpty = !props.draftMessage; @@ -353,6 +357,7 @@ function ReportActionItem(props) { {isHidden ? props.translate('moderation.revealMessage') : props.translate('moderation.hideMessage')} diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index e692e4668f07..abade067f4fc 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -36,6 +36,7 @@ const policySelector = (policy) => policy && { type: policy.type, role: policy.role, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, }; diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index f1633d62e490..4dd688950fbd 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -77,7 +77,10 @@ function MoneyRequestSelectorPage(props) { }; return ( - + {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js index 29f5baa173b4..0b5bf78142c5 100644 --- a/src/pages/iou/steps/NewRequestAmountPage.js +++ b/src/pages/iou/steps/NewRequestAmountPage.js @@ -177,6 +177,7 @@ function NewRequestAmountPage({route, iou, report}) { return ( {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js index 6780080ff382..29cefdc156a0 100644 --- a/src/pages/settings/Security/TwoFactorAuth/CodesPage.js +++ b/src/pages/settings/Security/TwoFactorAuth/CodesPage.js @@ -56,7 +56,10 @@ function CodesPage(props) { }, []); return ( - + + Task.dismissModalAndClearOutTaskInfo()} diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 10732a661dfc..ef601ca066c5 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -183,7 +183,7 @@ function WorkspaceInitialPage(props) { {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - shouldShow={_.isEmpty(props.policy) || !Policy.isPolicyOwner(props.policy)} + shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy)} subtitleKey={_.isEmpty(props.policy) ? undefined : 'workspace.common.notAuthorized'} > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index bff4a55d94a0..6a49e043a54a 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -201,7 +201,7 @@ function WorkspaceInvitePage(props) { const sections = didScreenTransitionEnd ? getSections() : []; return ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 154a337ee5a6..64cd9d319beb 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -405,7 +405,7 @@ function WorkspaceMembersPage(props) { > {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index 801277b89c9a..fa04f5cfc49e 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -5,8 +5,8 @@ import {withOnyx} from 'react-native-onyx'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import styles from '../../styles/styles'; +import * as PolicyUtils from '../../libs/PolicyUtils'; import Navigation from '../../libs/Navigation/Navigation'; -import * as Policy from '../../libs/actions/Policy'; import compose from '../../libs/compose'; import ROUTES from '../../ROUTES'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; @@ -105,7 +105,7 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal > Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - shouldShow={_.isEmpty(policy) || !Policy.isPolicyOwner(policy)} + shouldShow={_.isEmpty(policy) || !PolicyUtils.isPolicyAdmin(policy)} subtitleKey={_.isEmpty(policy) ? undefined : 'workspace.common.notAuthorized'} > { it('returns false when there is no report', () => { expect(ReportUtils.isWaitingForIOUActionFromCurrentUser()).toBe(false); }); - it('returns false when there is no reports collection', () => { + it('returns false when the matched IOU report does not have an owner accountID', () => { const report = { ...LHNTestUtils.getFakeReport(), - iouReportID: '1', + ownerAccountID: undefined, + hasOutstandingIOU: true, }; expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); - it('returns false when the report has no iouReportID', () => { - const report = LHNTestUtils.getFakeReport(); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}2`, { - reportID: '2', - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); - }); - }); - it('returns false when there is no matching IOU report', () => { - const report = { - ...LHNTestUtils.getFakeReport(), - iouReportID: '1', - }; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}2`, { - reportID: '2', - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); - }); - }); - it('returns false when the matched IOU report does not have an owner email', () => { - const report = { - ...LHNTestUtils.getFakeReport(), - iouReportID: '1', - }; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { - reportID: '1', - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); - }); - }); - it('returns false when the matched IOU report does not have an owner email', () => { + it('returns false when the linked iou report has an oustanding IOU', () => { const report = { ...LHNTestUtils.getFakeReport(), iouReportID: '1', @@ -333,52 +304,37 @@ describe('ReportUtils', () => { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { reportID: '1', ownerAccountID: 99, + hasOutstandingIOU: true, }).then(() => { expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); }); - it('returns true when the report has an oustanding IOU', () => { + it('returns true when the report has no oustanding IOU but is waiting for a bank account and the logged user is the report owner', () => { const report = { ...LHNTestUtils.getFakeReport(), - iouReportID: '1', - hasOutstandingIOU: true, + hasOutstandingIOU: false, + ownerAccountID: currentUserAccountID, + isWaitingOnBankAccount: true, }; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { - reportID: '1', - ownerAccountID: 99, - hasOutstandingIOU: true, - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); - }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); }); - it('returns false when the report has no oustanding IOU', () => { + it('returns true when the report has no oustanding IOU but is waiting for a bank account and the logged user is not the report owner', () => { const report = { ...LHNTestUtils.getFakeReport(), - iouReportID: '1', hasOutstandingIOU: false, + ownerAccountID: 97, + isWaitingOnBankAccount: true, }; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { - reportID: '1', - ownerAccountID: 99, - hasOutstandingIOU: false, - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); - }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); }); - it('returns true when the report has no oustanding IOU but is waiting for a bank account', () => { + it('returns true when the report has oustanding IOU', () => { const report = { ...LHNTestUtils.getFakeReport(), - iouReportID: '1', - hasOutstandingIOU: false, + ownerAccountID: 99, + hasOutstandingIOU: true, + isWaitingOnBankAccount: false, }; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, { - reportID: '1', - ownerAccountID: currentUserEmail, - hasOutstandingIOU: false, - isWaitingOnBankAccount: true, - }).then(() => { - expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(false); - }); + expect(ReportUtils.isWaitingForIOUActionFromCurrentUser(report)).toBe(true); }); }); diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 734f062ec770..ef56fa8783b8 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -385,7 +385,7 @@ describe('Sidebar', () => { }; const report3 = { ...LHNTestUtils.getFakeReport([5, 6], 1), - hasOutstandingIOU: true, + hasOutstandingIOU: false, // This has to be added after the IOU report is generated iouReportID: null, @@ -427,13 +427,12 @@ describe('Sidebar', () => { .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); - expect(displayNames).toHaveLength(4); + expect(displayNames).toHaveLength(3); expect(screen.queryAllByTestId('Pin Icon')).toHaveLength(1); expect(screen.queryAllByTestId('Pencil Icon')).toHaveLength(1); expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two owes $100.00'); - expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Five, Six'); - expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Three, Four'); + expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); }) ); }); @@ -700,21 +699,31 @@ describe('Sidebar', () => { // Given three IOU reports containing the same IOU amounts const report1 = { ...LHNTestUtils.getFakeReport([1, 2]), - hasOutstandingIOU: true, // This has to be added after the IOU report is generated iouReportID: null, }; const report2 = { ...LHNTestUtils.getFakeReport([3, 4]), - hasOutstandingIOU: true, // This has to be added after the IOU report is generated iouReportID: null, }; const report3 = { ...LHNTestUtils.getFakeReport([5, 6]), - hasOutstandingIOU: true, + hasOutstandingIOU: false, + + // This has to be added after the IOU report is generated + iouReportID: null, + }; + const report4 = { + ...LHNTestUtils.getFakeReport([5, 6]), + + // This has to be added after the IOU report is generated + iouReportID: null, + }; + const report5 = { + ...LHNTestUtils.getFakeReport([5, 6]), // This has to be added after the IOU report is generated iouReportID: null, @@ -733,7 +742,7 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport([9, 10]), type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, - managerID: 2, + managerID: 3, hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -743,7 +752,27 @@ describe('Sidebar', () => { ...LHNTestUtils.getFakeReport([11, 12]), type: CONST.REPORT.TYPE.IOU, ownerAccountID: 2, - managerID: 2, + managerID: 4, + hasOutstandingIOU: true, + total: 100000, + currency: 'USD', + chatReportID: report3.reportID, + }; + const iouReport4 = { + ...LHNTestUtils.getFakeReport([11, 12]), + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: 2, + managerID: 5, + hasOutstandingIOU: true, + total: 10000, + currency: 'USD', + chatReportID: report3.reportID, + }; + const iouReport5 = { + ...LHNTestUtils.getFakeReport([11, 12]), + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: 2, + managerID: 6, hasOutstandingIOU: true, total: 10000, currency: 'USD', @@ -753,6 +782,8 @@ describe('Sidebar', () => { report1.iouReportID = iouReport1.reportID; report2.iouReportID = iouReport2.reportID; report3.iouReportID = iouReport3.reportID; + report4.iouReportID = iouReport4.reportID; + report5.iouReportID = iouReport5.reportID; const currentlyLoggedInUserAccountID = 13; LHNTestUtils.getDefaultRenderedSidebarLinks('0'); @@ -768,22 +799,26 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + [`${ONYXKEYS.COLLECTION.REPORT}${report4.reportID}`]: report4, + [`${ONYXKEYS.COLLECTION.REPORT}${report5.reportID}`]: report5, [`${ONYXKEYS.COLLECTION.REPORT}${iouReport1.reportID}`]: iouReport1, [`${ONYXKEYS.COLLECTION.REPORT}${iouReport2.reportID}`]: iouReport2, [`${ONYXKEYS.COLLECTION.REPORT}${iouReport3.reportID}`]: iouReport3, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport4.reportID}`]: iouReport4, + [`${ONYXKEYS.COLLECTION.REPORT}${iouReport5.reportID}`]: iouReport5, }), ) - // Then the reports are ordered alphabetically since their amounts are the same + // Then the reports with the same amount are ordered alphabetically .then(() => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); expect(displayNames).toHaveLength(5); - expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Two owes $100.00'); - expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Two owes $100.00'); - expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Two owes $100.00'); - expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Five, Six'); - expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('One, Two'); + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('Email Four owes $1,000.00'); + expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Email Five owes $100.00'); + expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Email Six owes $100.00'); + expect(lodashGet(displayNames, [3, 'props', 'children'])).toBe('Email Three owes $100.00'); + expect(lodashGet(displayNames, [4, 'props', 'children'])).toBe('Email Two owes $100.00'); }) ); });