diff --git a/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js b/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js index 9c8ae24a2749..9f582c2df9d6 100644 --- a/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js +++ b/src/libs/Navigation/AppNavigator/MainDrawerNavigator.js @@ -9,6 +9,7 @@ import SCREENS from '../../../SCREENS'; import Permissions from '../../Permissions'; import Timing from '../../actions/Timing'; import CONST from '../../../CONST'; +import * as App from '../../actions/App'; // Screens import ReportScreen from '../../../pages/home/ReportScreen'; @@ -35,6 +36,8 @@ const propTypes = { type: PropTypes.string, })), + isFirstTimeNewExpensifyUser: PropTypes.bool, + route: PropTypes.shape({ params: PropTypes.shape({ openOnAdminRoom: PropTypes.bool, @@ -48,6 +51,7 @@ const defaultProps = { reports: {}, betas: [], policies: {}, + isFirstTimeNewExpensifyUser: false, }; /** @@ -56,11 +60,12 @@ const defaultProps = { * @param {Object} reports * @param {Boolean} [ignoreDefaultRooms] * @param {Object} policies + * @param {Boolean} isFirstTimeNewExpensifyUser * @param {Boolean} openOnAdminRoom * @returns {Object} */ -const getInitialReportScreenParams = (reports, ignoreDefaultRooms, policies, openOnAdminRoom) => { - const last = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, openOnAdminRoom); +const getInitialReportScreenParams = (reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom) => { + const last = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom); // Fallback to empty if for some reason reportID cannot be derived - prevents the app from crashing const reportID = lodashGet(last, 'reportID', ''); @@ -75,6 +80,7 @@ class MainDrawerNavigator extends Component { props.reports, !Permissions.canUseDefaultRooms(props.betas), props.policies, + props.isFirstTimeNewExpensifyUser, lodashGet(props, 'route.params.openOnAdminRoom', false), ); @@ -88,9 +94,14 @@ class MainDrawerNavigator extends Component { nextProps.reports, !Permissions.canUseDefaultRooms(nextProps.betas), nextProps.policies, + nextProps.isFirstTimeNewExpensifyUser, lodashGet(nextProps, 'route.params.openOnAdminRoom', false), ); if (this.initialParams.reportID === initialNextParams.reportID) { + // We need to wait to open the app until this check is made, since there's a race condition that can happen + // where OpenApp will get called beforehand, setting isFirstTimeNewExpensifyUser to false and causing us + // to miss the deep-linked report in ReportUtils.findLastAccessedReport + App.confirmReadyToOpenApp(); return false; } @@ -156,4 +167,7 @@ export default withOnyx({ policies: { key: ONYXKEYS.COLLECTION.POLICY, }, + isFirstTimeNewExpensifyUser: { + key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + }, })(MainDrawerNavigator); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 398cd025e598..328c1bf4c775 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -257,6 +257,17 @@ function isPublicRoom(report) { return visibility === CONST.REPORT.VISIBILITY.PUBLIC || visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; } +/** + * Whether the provided report is a public announce room + * @param {Object} report + * @param {String} report.visibility + * @returns {Boolean} + */ +function isPublicAnnounceRoom(report) { + const visibility = lodashGet(report, 'visibility', ''); + return visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE; +} + /** * Get the policy type from a given report * @param {Object} report @@ -277,18 +288,44 @@ function hasExpensifyGuidesEmails(emails) { return _.some(emails, email => Str.extractEmailDomain(email) === CONST.EMAIL.GUIDES_DOMAIN); } +/** + * Only returns true if this is our main 1:1 DM report with Concierge + * + * @param {Object} report + * @returns {Boolean} + */ +function isConciergeChatReport(report) { + return lodashGet(report, 'participants', []).length === 1 + && report.participants[0] === CONST.EMAIL.CONCIERGE; +} + /** * @param {Record|Array<{lastReadTime, reportID}>} reports * @param {Boolean} [ignoreDefaultRooms] * @param {Object} policies + * @param {Boolean} isFirstTimeNewExpensifyUser * @param {Boolean} openOnAdminRoom * @returns {Object} */ -function findLastAccessedReport(reports, ignoreDefaultRooms, policies, openOnAdminRoom = false) { +function findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom = false) { + // If it's the user's first time using New Expensify, then they could either have: + // - just a Concierge report, if so we'll return that + // - their Concierge report, and a separate report that must have deeplinked them to the app before they created their account. + // If it's the latter, we'll use the deeplinked report over the Concierge report, + // since the Concierge report would be incorrectly selected over the deep-linked report in the logic below. let sortedReports = sortReportsByLastRead(reports); + if (isFirstTimeNewExpensifyUser) { + if (sortedReports.length === 1) { + return sortedReports[0]; + } + return _.find(sortedReports, report => !isConciergeChatReport(report)); + } + if (ignoreDefaultRooms) { - sortedReports = _.filter(sortedReports, report => !isDefaultRoom(report) + // We allow public announce rooms to show as the last accessed report since we bypass the default rooms beta for them. + // Check where ReportUtils.findLastAccessedReport is called in MainDrawerNavigator.js for more context. + sortedReports = _.filter(sortedReports, report => !isDefaultRoom(report) || isPublicAnnounceRoom(report) || getPolicyType(report, policies) === CONST.POLICY.TYPE.FREE || hasExpensifyGuidesEmails(lodashGet(report, ['participants'], []))); } @@ -419,17 +456,6 @@ function getRoomWelcomeMessage(report, policiesMap) { return welcomeMessage; } -/** - * Only returns true if this is our main 1:1 DM report with Concierge - * - * @param {Object} report - * @returns {Boolean} - */ -function isConciergeChatReport(report) { - return lodashGet(report, 'participants', []).length === 1 - && report.participants[0] === CONST.EMAIL.CONCIERGE; -} - /** * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) * @param {Object} report @@ -1707,7 +1733,7 @@ function canLeaveRoom(report, isPolicyMember) { || _.isEmpty(report.chatType)) { // DM chats don't have a chatType return false; } - } else if (report.visibility === CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE && isPolicyMember) { + } else if (isPublicAnnounceRoom(report) && isPolicyMember) { return false; } return true; @@ -1758,6 +1784,7 @@ export { isArchivedRoom, isPolicyExpenseChatAdmin, isPublicRoom, + isPublicAnnounceRoom, isConciergeChatReport, isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyEmails, diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 30c825f40f90..2790765745be 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -58,6 +58,15 @@ Onyx.connect({ callback: val => preferredLocale = val, }); +let resolveIsReadyPromise; +const isReadyToOpenApp = new Promise((resolve) => { + resolveIsReadyPromise = resolve; +}); + +function confirmReadyToOpenApp() { + resolveIsReadyPromise(); +} + /** * @param {Array} policies * @return {Array} array of policy ids @@ -128,36 +137,38 @@ AppState.addEventListener('change', (nextAppState) => { * Fetches data needed for app initialization */ function openApp() { - // We need a fresh connection/callback here to make sure that the list of policyIDs that is sent to OpenApp is the most updated list from Onyx - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (policies) => { - Onyx.disconnect(connectionID); - API.read('OpenApp', {policyIDList: getNonOptimisticPolicyIDs(policies)}, { - optimisticData: [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: true, - }, - ], - successData: [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - failureData: [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - }); - }, + isReadyToOpenApp.then(() => { + // We need a fresh connection/callback here to make sure that the list of policyIDs that is sent to OpenApp is the most updated list from Onyx + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (policies) => { + Onyx.disconnect(connectionID); + API.read('OpenApp', {policyIDList: getNonOptimisticPolicyIDs(policies)}, { + optimisticData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: true, + }, + ], + successData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + failureData: [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + }); + }, + }); }); } @@ -294,4 +305,5 @@ export { openProfile, openApp, reconnectApp, + confirmReadyToOpenApp, }; diff --git a/tests/actions/SessionTest.js b/tests/actions/SessionTest.js index 62a7e183647d..2a91b0b4ad61 100644 --- a/tests/actions/SessionTest.js +++ b/tests/actions/SessionTest.js @@ -75,6 +75,7 @@ describe('Session', () => { })); // When we attempt to fetch the initial app data via the API + App.confirmReadyToOpenApp(); App.openApp(); return waitForPromisesToResolve(); }) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 0f4772ad602a..a5458f356579 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -97,6 +97,7 @@ describe('NetworkTests', () => { })); // This should first trigger re-authentication and then a Failed to fetch + App.confirmReadyToOpenApp(); App.openApp(); return waitForPromisesToResolve() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}))