diff --git a/src/components/PromotedActionsBar.tsx b/src/components/PromotedActionsBar.tsx index 42084bda3361..b25fb2fd1d75 100644 --- a/src/components/PromotedActionsBar.tsx +++ b/src/components/PromotedActionsBar.tsx @@ -32,6 +32,7 @@ type PromotedActionsType = Record P login?: string; currentUserAccountID: number; introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; }) => PromotedAction; } & { [CONST.PROMOTED_ACTIONS.JOIN]: (report: OnyxReport, currentUserAccountID: number) => PromotedAction; @@ -63,7 +64,7 @@ const PromotedActions = { joinRoom(report, currentUserAccountID); }), }), - message: ({reportID, accountID, login, currentUserAccountID, introSelected}) => ({ + message: ({reportID, accountID, login, currentUserAccountID, introSelected, isSelfTourViewed}) => ({ key: CONST.PROMOTED_ACTIONS.MESSAGE, icon: 'CommentBubbles', translationKey: 'common.message', @@ -75,7 +76,7 @@ const PromotedActions = { // The accountID might be optimistic, so we should use the login if we have it if (login) { - navigateToAndOpenReport([login], currentUserAccountID, introSelected, false); + navigateToAndOpenReport([login], currentUserAccountID, introSelected, isSelfTourViewed, false); return; } if (accountID) { diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index 06f5a83f9c14..9ce71dad811a 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -1,4 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; +import {hasSeenTourSelector} from '@selectors/Onboarding'; import {accountIDSelector} from '@selectors/Session'; import {deepEqual} from 'fast-equals'; import isEmpty from 'lodash/isEmpty'; @@ -70,6 +71,7 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo const {inputQuery: originalInputQuery} = queryJSON; const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const queryText = buildUserReadableQueryString({ queryJSON, PersonalDetails: personalDetails, @@ -242,10 +244,10 @@ function SearchPageHeaderInput({queryJSON, searchRouterListVisible, hideSearchRo } else if (item?.reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); } else if ('login' in item) { - navigateToAndOpenReport(item.login ? [item.login] : [], currentUserAccountID, introSelected, false); + navigateToAndOpenReport(item.login ? [item.login] : [], currentUserAccountID, introSelected, isSelfTourViewed, false); } }, - [autocompleteSubstitutions, onSearchQueryChange, submitSearch, textInputValue, currentUserAccountID, introSelected], + [autocompleteSubstitutions, onSearchQueryChange, submitSearch, textInputValue, currentUserAccountID, introSelected, isSelfTourViewed], ); const searchQueryItem = useMemo( diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index a04fdfe81f9c..787bd0bf5520 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -1,3 +1,4 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; import {deepEqual} from 'fast-equals'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {TextInputProps} from 'react-native'; @@ -63,6 +64,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const currentUserAccountID = currentUserPersonalDetails.accountID; const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -288,13 +290,13 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla if (item?.reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item.reportID)); } else if ('login' in item) { - navigateToAndOpenReport(item.login ? [item.login] : [], currentUserAccountID, introSelected, false); + navigateToAndOpenReport(item.login ? [item.login] : [], currentUserAccountID, introSelected, isSelfTourViewed, false); } }); onRouterClose(); } }, - [autocompleteSubstitutions, onRouterClose, onSearchQueryChange, policies, reports, submitSearch, textInputValue, currentUserAccountID, introSelected], + [autocompleteSubstitutions, onRouterClose, onSearchQueryChange, policies, reports, submitSearch, textInputValue, currentUserAccountID, introSelected, isSelfTourViewed], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index f4e03d26c5a2..64ab9f1b60f0 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -317,6 +317,9 @@ type OpenReportActionParams = { /** The current user's account ID */ currentUserAccountID?: number; + /** Whether the user has seen the self tour */ + // TODO: This will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66424 + isSelfTourViewed?: boolean; /** Beta features list. TODO: Remove optional (?) once buildPolicyData is updated (https://github.com/Expensify/App/issues/66417) */ betas?: OnyxEntry; }; @@ -1168,7 +1171,12 @@ type GuidedSetupDataForOpenReport = { * Returns the onyx data arrays and guidedSetupData string to include in the API parameters, * or undefined if no guided setup is needed. */ -function getGuidedSetupDataForOpenReport(introSelected: OnyxEntry, betas: OnyxEntry): GuidedSetupDataForOpenReport | undefined { +function getGuidedSetupDataForOpenReport( + introSelected: OnyxEntry, + betas: OnyxEntry, + // TODO: This will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66424 + isSelfTourViewed?: boolean, +): GuidedSetupDataForOpenReport | undefined { const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false; const isOnboardingCompleted = onboarding?.hasCompletedGuidedSetupFlow ?? false; @@ -1202,6 +1210,7 @@ function getGuidedSetupDataForOpenReport(introSelected: OnyxEntry engagementChoice: choice, onboardingMessage, companySize: introSelected?.companySize as OnboardingCompanySize, + isSelfTourViewed, betas, }); @@ -1248,6 +1257,7 @@ function openReport(params: OpenReportActionParams) { optimisticSelfDMReport, currentUserLogin, currentUserAccountID, + isSelfTourViewed, betas, } = params; if (!reportID) { @@ -1470,7 +1480,7 @@ function openReport(params: OpenReportActionParams) { }); } - const guidedSetup = getGuidedSetupDataForOpenReport(introSelected, betas); + const guidedSetup = getGuidedSetupDataForOpenReport(introSelected, betas, isSelfTourViewed); if (guidedSetup) { optimisticData.push(...guidedSetup.optimisticData); successData.push(...guidedSetup.successData); @@ -1956,7 +1966,13 @@ function navigateToReport(reportID: string | undefined, shouldDismissModal = tru * @param currentUserAccountID the account ID of the current user. * @param shouldDismissModal a flag to determine if we should dismiss modal before navigate to report or navigate to report directly. */ -function navigateToAndOpenReport(userLogins: string[], currentUserAccountID: number, introSelected: OnyxEntry, shouldDismissModal = true) { +function navigateToAndOpenReport( + userLogins: string[], + currentUserAccountID: number, + introSelected: OnyxEntry, + isSelfTourViewed: boolean | undefined, + shouldDismissModal = true, +) { let newChat: OptimisticChatReport | undefined; const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins(userLogins); const chat = getChatByParticipants([...participantAccountIDs, currentUserAccountID]); @@ -1967,7 +1983,7 @@ function navigateToAndOpenReport(userLogins: string[], currentUserAccountID: num notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, }); // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport({reportID: newChat?.reportID, introSelected, reportActionID: '', participantLoginList: userLogins, newReportObject: newChat}); + openReport({reportID: newChat?.reportID, introSelected, reportActionID: '', participantLoginList: userLogins, newReportObject: newChat, isSelfTourViewed}); } const report = isEmptyObject(chat) ? newChat : chat; @@ -3343,7 +3359,8 @@ function navigateToConciergeChat( if (!checkIfCurrentPageActive()) { return; } - navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], currentUserAccountID, introSelected, shouldDismissModal); + // TODO: We'll pass isSelfTourViewed in the next PR. Refactor issue: https://github.com/Expensify/App/issues/66424 + navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], currentUserAccountID, introSelected, undefined, shouldDismissModal); }); } else if (shouldDismissModal) { Navigation.dismissModalWithReport({reportID: conciergeReportID, reportActionID}); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 53e490aeb87d..ddaf2206f10e 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -1,4 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; +import {hasSeenTourSelector} from '@selectors/Onboarding'; import isEmpty from 'lodash/isEmpty'; import reject from 'lodash/reject'; import type {Ref} from 'react'; @@ -242,6 +243,7 @@ function NewChatPage({ref}: NewChatPageProps) { const {top} = useSafeAreaInsets(); const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const [reportAttributesDerivedFull] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES); const privateIsArchivedMap = usePrivateIsArchivedMap(); @@ -402,7 +404,7 @@ function NewChatPage({ref}: NewChatPageProps) { return; } KeyboardUtils.dismiss().then(() => { - singleExecution(() => navigateToAndOpenReport([login], currentUserAccountID, introSelected))(); + singleExecution(() => navigateToAndOpenReport([login], currentUserAccountID, introSelected, isSelfTourViewed))(); }); }; diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 6a710feecb96..844669840987 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -1,3 +1,4 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; import {Str} from 'expensify-common'; import React, {useEffect} from 'react'; import {View} from 'react-native'; @@ -75,6 +76,7 @@ function ProfilePage({route}: ProfilePageProps) { const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [isDebugModeEnabled = false] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const guideCalendarLink = account?.guideDetails?.calendarLink ?? ''; const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bug', 'Pencil', 'Phone']); const accountID = Number(route.params?.accountID ?? CONST.DEFAULT_NUMBER_ID); @@ -163,7 +165,7 @@ function ProfilePage({route}: ProfilePageProps) { // If it's a self DM, we only want to show the Message button if the self DM report exists because we don't want to optimistically create a report for self DM if ((!isCurrentUser || report) && !isAnonymousUserSession()) { - promotedActions.push(PromotedActions.message({reportID: report?.reportID, accountID, login: loginParams, currentUserAccountID, introSelected})); + promotedActions.push(PromotedActions.message({reportID: report?.reportID, accountID, login: loginParams, currentUserAccountID, introSelected, isSelfTourViewed})); } return ( diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 0c9e44f991c7..a2e43ca2c69a 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -5465,7 +5465,7 @@ describe('actions/Report', () => { const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; // When navigateToAndOpenReport is called with a participant that doesn't have an existing chat - Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected); + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, false); await waitForBatchedUpdates(); // Then verify OpenReport API was called @@ -5511,7 +5511,7 @@ describe('actions/Report', () => { const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; // When navigateToAndOpenReport is called with the participant that has an existing chat - Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected); + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, false); await waitForBatchedUpdates(); // Then verify OpenReport API was NOT called since the chat already exists @@ -5541,7 +5541,7 @@ describe('actions/Report', () => { const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN, isInviteOnboardingComplete: false}; // When navigateToAndOpenReport is called with introSelected - Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected); + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, false); await waitForBatchedUpdates(); // Then verify OpenReport API was called (new chat created) @@ -5571,9 +5571,96 @@ describe('actions/Report', () => { const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; // When navigateToAndOpenReport is called with shouldDismissModal=false - Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, false); + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, false, false); + await waitForBatchedUpdates(); + + // Then verify navigation was called + expect(Navigation.navigate).toHaveBeenCalled(); + }); + + it('should pass isSelfTourViewed=true through to openReport when creating new chat', async () => { + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@user.com'; + const PARTICIPANT_LOGIN = 'participant@test.com'; + const PARTICIPANT_ACCOUNT_ID = 2; + + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [PARTICIPANT_ACCOUNT_ID]: { + accountID: PARTICIPANT_ACCOUNT_ID, + login: PARTICIPANT_LOGIN, + displayName: 'Participant', + }, + }); + + const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; + + // When navigateToAndOpenReport is called with isSelfTourViewed=true + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, true); + await waitForBatchedUpdates(); + + // Then verify OpenReport API was called + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.OPEN_REPORT, 1); + + // Then verify navigation was called + expect(Navigation.navigate).toHaveBeenCalled(); + }); + + it('should pass isSelfTourViewed=undefined through to openReport when creating new chat', async () => { + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@user.com'; + const PARTICIPANT_LOGIN = 'participant@test.com'; + const PARTICIPANT_ACCOUNT_ID = 2; + + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [PARTICIPANT_ACCOUNT_ID]: { + accountID: PARTICIPANT_ACCOUNT_ID, + login: PARTICIPANT_LOGIN, + displayName: 'Participant', + }, + }); + + const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; + + // When navigateToAndOpenReport is called with isSelfTourViewed=undefined + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, undefined); await waitForBatchedUpdates(); + // Then verify OpenReport API was called + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.OPEN_REPORT, 1); + + // Then verify navigation was called + expect(Navigation.navigate).toHaveBeenCalled(); + }); + + it('should pass isSelfTourViewed along with shouldDismissModal', async () => { + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@user.com'; + const PARTICIPANT_LOGIN = 'participant@test.com'; + const PARTICIPANT_ACCOUNT_ID = 2; + + await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [PARTICIPANT_ACCOUNT_ID]: { + accountID: PARTICIPANT_ACCOUNT_ID, + login: PARTICIPANT_LOGIN, + displayName: 'Participant', + }, + }); + + const testIntroSelected: OnyxTypes.IntroSelected = {choice: CONST.ONBOARDING_CHOICES.ADMIN}; + + // When navigateToAndOpenReport is called with isSelfTourViewed=true and shouldDismissModal=false + Report.navigateToAndOpenReport([PARTICIPANT_LOGIN], TEST_USER_ACCOUNT_ID, testIntroSelected, true, false); + await waitForBatchedUpdates(); + + // Then verify OpenReport API was called + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.OPEN_REPORT, 1); + // Then verify navigation was called expect(Navigation.navigate).toHaveBeenCalled(); }); diff --git a/tests/unit/PromotedActionsBarTest.ts b/tests/unit/PromotedActionsBarTest.ts index 009d8b1359a2..e48d720dc14a 100644 --- a/tests/unit/PromotedActionsBarTest.ts +++ b/tests/unit/PromotedActionsBarTest.ts @@ -31,11 +31,12 @@ describe('PromotedActions.message', () => { login: 'test@example.com', currentUserAccountID: 1, introSelected, + isSelfTourViewed: false, }); action.onSelected(); - expect(mockNavigateToAndOpenReport).toHaveBeenCalledWith(['test@example.com'], 1, introSelected, false); + expect(mockNavigateToAndOpenReport).toHaveBeenCalledWith(['test@example.com'], 1, introSelected, false, false); }); it('should pass introSelected to navigateToAndOpenReportWithAccountIDs when accountID is provided', () => { @@ -44,6 +45,7 @@ describe('PromotedActions.message', () => { accountID: 42, currentUserAccountID: 1, introSelected, + isSelfTourViewed: false, }); action.onSelected(); @@ -56,6 +58,7 @@ describe('PromotedActions.message', () => { accountID: 42, currentUserAccountID: 1, introSelected: undefined, + isSelfTourViewed: undefined, }); action.onSelected(); @@ -68,6 +71,7 @@ describe('PromotedActions.message', () => { reportID: 'report123', currentUserAccountID: 1, introSelected: undefined, + isSelfTourViewed: undefined, }); action.onSelected(); @@ -83,11 +87,12 @@ describe('PromotedActions.message', () => { login: 'test@example.com', currentUserAccountID: 1, introSelected, + isSelfTourViewed: false, }); action.onSelected(); - expect(mockNavigateToAndOpenReport).toHaveBeenCalledWith(['test@example.com'], 1, introSelected, false); + expect(mockNavigateToAndOpenReport).toHaveBeenCalledWith(['test@example.com'], 1, introSelected, false, false); expect(mockNavigateToAndOpenReportWithAccountIDs).not.toHaveBeenCalled(); }); });