diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 686724c50c80..6f8af855a6d5 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -69,6 +69,7 @@ import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'r /* eslint-disable rulesdir/prefer-early-return */ import {useIsFocused, useRoute} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import isEmpty from 'lodash/isEmpty'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, View} from 'react-native'; @@ -170,6 +171,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const transactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActions ?? [], false, reportTransactionIDs); const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]); @@ -684,7 +686,16 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) }, 2000); if (!hasNewestReportAction) { - openReport({reportID, introSelected, conciergeChat, betas, hasReportActions: true, currentUserAccountID}); + openReport({ + reportID, + introSelected, + conciergeChat, + betas, + hasReportActions: true, + currentUserAccountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); scrollToBottom(); return; } @@ -692,7 +703,18 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps) // Defer marking the report as read until the scroll actually reaches the bottom (handled in onTrackScrolling). pendingMarkAsReadRef.current = true; scrollToBottom(); - }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, scrollToBottom, reportID, introSelected, conciergeChat, betas, currentUserAccountID]); + }, [ + setIsFloatingMessageCounterVisible, + hasNewestReportAction, + scrollToBottom, + reportID, + introSelected, + conciergeChat, + betas, + currentUserAccountID, + guidedSetupAndTourStatus?.isSelfTourViewed, + guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + ]); useEffect(() => { return () => { diff --git a/src/hooks/useReportActionsScroll.ts b/src/hooks/useReportActionsScroll.ts index ce22a0d17600..81b146b2f764 100644 --- a/src/hooks/useReportActionsScroll.ts +++ b/src/hooks/useReportActionsScroll.ts @@ -25,6 +25,7 @@ import type {NativeScrollEvent, NativeSyntheticEvent, ViewToken} from 'react-nat import type {OnyxEntry} from 'react-native-onyx'; import {useRoute} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {useEffect, useEffectEvent, useState} from 'react'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; @@ -167,6 +168,7 @@ function useReportActionsScroll({ const {isOffline} = useNetworkWithOfflineStatus(); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const [reportLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${reportID}`); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${getNonEmptyStringOnyxID(reportID)}`); @@ -352,7 +354,16 @@ function useReportActionsScroll({ if (!Navigation.getReportRHPActiveRoute()) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, undefined, backTo)); } - openReport({reportID, introSelected, conciergeChat, betas, hasReportActions: true, currentUserAccountID}); + openReport({ + reportID, + introSelected, + conciergeChat, + betas, + hasReportActions: true, + currentUserAccountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); reportScrollManager.scrollToBottom(); return; } diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 753b6f49a1b7..2b379809498f 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -51,6 +51,7 @@ import type {Transaction, TransactionViolations} from '@src/types/onyx'; import {PortalHost} from '@gorhom/portal'; import {useIsFocused} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import React, {useEffect, useMemo, useRef} from 'react'; type SearchMoneyRequestPageProps = @@ -128,6 +129,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const {transactions: allReportTransactions, violations: allReportViolations} = useTransactionsAndViolationsForReport(reportIDFromRoute); const {transactionThreadReportID, effectiveTransactionThreadReportID, reportActions} = useTransactionThreadReportID(reportIDFromRoute); const reportTransactions = useMemo(() => getAllNonDeletedTransactions(allReportTransactions, reportActions), [allReportTransactions, reportActions]); @@ -215,7 +217,16 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return; } - openReport({reportID: reportIDFromRoute, introSelected, conciergeChat, betas, hasReportActions, currentUserAccountID}); + openReport({ + reportID: reportIDFromRoute, + introSelected, + conciergeChat, + betas, + hasReportActions, + currentUserAccountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); isInitialMountRef.current = false; // oneTransactionID dependency handles the case when deleting a transaction: diff --git a/src/pages/Share/ShareDetailsPage.tsx b/src/pages/Share/ShareDetailsPage.tsx index 970819d1ff29..d1074c365177 100644 --- a/src/pages/Share/ShareDetailsPage.tsx +++ b/src/pages/Share/ShareDetailsPage.tsx @@ -43,6 +43,7 @@ import KeyboardUtils from '@src/utils/keyboard'; import type {StackScreenProps} from '@react-navigation/stack'; import type {OnyxEntry} from 'react-native-onyx'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {isDraftReportSelector} from '@selectors/Report'; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -63,6 +64,7 @@ function ShareDetailsPage({route}: ShareDetailsPageProps) { const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE); const [validatedFile] = useOnyx(ONYXKEYS.VALIDATED_FILE_OBJECT); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const [isDraftReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportOrAccountID}`, {selector: isDraftReportSelector}); const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); @@ -177,6 +179,8 @@ function ShareDetailsPage({route}: ShareDetailsPageProps) { betas, hasReportActions: false, currentUserAccountID: personalDetail.accountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, }); } if (report.reportID) { diff --git a/src/pages/TransactionDuplicate/DynamicReviewPage.tsx b/src/pages/TransactionDuplicate/DynamicReviewPage.tsx index c0e3a9188780..58e0254f91e8 100644 --- a/src/pages/TransactionDuplicate/DynamicReviewPage.tsx +++ b/src/pages/TransactionDuplicate/DynamicReviewPage.tsx @@ -38,6 +38,7 @@ import type {Transaction} from '@src/types/onyx'; import getEmptyArray from '@src/types/utils/getEmptyArray'; import {useFocusEffect, useRoute} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -67,6 +68,7 @@ function DynamicReviewPage() { const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const originalTransactionIDsListRef = useRef(null); @@ -120,8 +122,27 @@ function DynamicReviewPage() { if (!route.params.reportID || report?.reportID) { return; } - openReport({reportID: route.params.reportID, introSelected, conciergeChat, betas, hasReportActions, currentUserAccountID: currentPersonalDetails.accountID}); - }, [report?.reportID, route.params.reportID, introSelected, conciergeChat, betas, hasReportActions, currentPersonalDetails.accountID]); + openReport({ + reportID: route.params.reportID, + introSelected, + conciergeChat, + betas, + hasReportActions, + currentUserAccountID: currentPersonalDetails.accountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); + }, [ + report?.reportID, + route.params.reportID, + introSelected, + conciergeChat, + betas, + hasReportActions, + currentPersonalDetails.accountID, + guidedSetupAndTourStatus?.isSelfTourViewed, + guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + ]); useEffect(() => { if (!transactionID) { diff --git a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx index 43632c816f2b..c73b71ecd07b 100644 --- a/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx +++ b/src/pages/workspace/rooms/WorkspaceRoomsPage.tsx @@ -34,6 +34,7 @@ import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {useFocusEffect} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {policyChatRoomsSelector} from '@selectors/Report'; import React from 'react'; import {View} from 'react-native'; @@ -57,6 +58,7 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const [policyReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: policyChatRoomsSelector(policyID, reportNameValuePairs)}); @@ -97,6 +99,8 @@ function WorkspaceRoomsPage({route}: WorkspaceRoomsPageProps) { shouldMarkAsRead: false, hasReportActions: !!hasReportActions?.[report.reportID], currentUserAccountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, }); Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.REPORT_DETAILS.getRoute(report.reportID))); return; diff --git a/tests/unit/OnboardingSelectorsTest.ts b/tests/unit/OnboardingSelectorsTest.ts index 690aac2b01d0..c2d38a815d95 100644 --- a/tests/unit/OnboardingSelectorsTest.ts +++ b/tests/unit/OnboardingSelectorsTest.ts @@ -3,7 +3,7 @@ import type ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxValue} from 'react-native-onyx'; -import {hasCompletedGuidedSetupFlowSelector, hasSeenTourSelector, isTrackIntentUserSelector} from '@selectors/Onboarding'; +import {guidedSetupAndTourStatusSelector, hasCompletedGuidedSetupFlowSelector, hasSeenTourSelector, isTrackIntentUserSelector} from '@selectors/Onboarding'; import createMock from '../utils/createMock'; @@ -93,4 +93,27 @@ describe('onboardingSelectors', () => { expect(isTrackIntentUserSelector(introSelected)).toBe(false); }); }); + + // The combined selector derives both onboarding flags from a single NVP_ONBOARDING read. Callers that need both + // (e.g. the openReport wiring) rely on each field mapping to its own source without being swapped. + describe('guidedSetupAndTourStatusSelector', () => { + it('Should map each flag from its own source without swapping them', () => { + const onboarding = createMock>>({selfTourViewed: true, hasCompletedGuidedSetupFlow: false}); + expect(guidedSetupAndTourStatusSelector(onboarding)).toEqual({isSelfTourViewed: true, hasCompletedGuidedSetupFlow: false}); + }); + + it('Should treat an empty onboarding NVP as tour-not-seen and guided-setup-completed', () => { + const onboarding = createMock>>({}); + expect(guidedSetupAndTourStatusSelector(onboarding)).toEqual({isSelfTourViewed: false, hasCompletedGuidedSetupFlow: true}); + }); + + it('Should treat an undefined onboarding NVP the same as an empty one', () => { + expect(guidedSetupAndTourStatusSelector(undefined)).toEqual({isSelfTourViewed: false, hasCompletedGuidedSetupFlow: true}); + }); + + it('Should reflect a fully completed onboarding (tour seen and guided setup done)', () => { + const onboarding = createMock>>({selfTourViewed: true, hasCompletedGuidedSetupFlow: true}); + expect(guidedSetupAndTourStatusSelector(onboarding)).toEqual({isSelfTourViewed: true, hasCompletedGuidedSetupFlow: true}); + }); + }); });