diff --git a/src/DeepLinkHandler.tsx b/src/DeepLinkHandler.tsx index b581317c3e81..42aeda6760f1 100644 --- a/src/DeepLinkHandler.tsx +++ b/src/DeepLinkHandler.tsx @@ -16,7 +16,7 @@ import {getReportIDFromLink} from './libs/ReportUtils'; import {endSpan} from './libs/telemetry/activeSpans'; import {hasSecureLinkKey} from './libs/Url'; import ONYXKEYS from './ONYXKEYS'; -import {hasSeenTourSelector} from './selectors/Onboarding'; +import {guidedSetupAndTourStatusSelector} from './selectors/Onboarding'; import isLoadingOnyxValue from './types/utils/isLoadingOnyxValue'; type DeepLinkHandlerProps = { @@ -40,7 +40,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { const [session, sessionMetadata] = useOnyx(ONYXKEYS.SESSION); const [conciergeReportID, conciergeReportIDMetadata] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected, introSelectedMetadata] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isSelfTourViewed, isSelfTourViewedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [guidedSetupAndTourStatus, guidedSetupAndTourStatusMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const [betas, betasMetadata] = useOnyx(ONYXKEYS.BETAS); const isAuthenticated = useIsAuthenticated(); @@ -56,7 +56,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { }, []); useEffect(() => { - if (isLoadingOnyxValue(allReportsMetadata, sessionMetadata, conciergeReportIDMetadata, introSelectedMetadata, isSelfTourViewedMetadata, betasMetadata)) { + if (isLoadingOnyxValue(allReportsMetadata, sessionMetadata, conciergeReportIDMetadata, introSelectedMetadata, guidedSetupAndTourStatusMetadata, betasMetadata)) { return; } @@ -110,7 +110,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { isCurrentlyAuthenticated, conciergeReportID, introSelected, - isSelfTourViewed, + guidedSetupAndTourStatus?.isSelfTourViewed, betas, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, ); @@ -147,7 +147,16 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { if (hasSecureLinkKey(state.url)) { onInitialUrl(state.url as Route); } - openReportFromDeepLink(state.url, allReports, isCurrentlyAuthenticated, conciergeReportID, introSelected, isSelfTourViewed, betas, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); + openReportFromDeepLink( + state.url, + allReports, + isCurrentlyAuthenticated, + conciergeReportID, + introSelected, + guidedSetupAndTourStatus?.isSelfTourViewed, + betas, + session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + ); trackPendingPublicRoomFromDeepLink(state.url, isCurrentlyAuthenticated); }); @@ -165,7 +174,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { sessionMetadata.status, conciergeReportIDMetadata.status, introSelectedMetadata.status, - isSelfTourViewedMetadata.status, + guidedSetupAndTourStatusMetadata.status, betasMetadata.status, ]); @@ -200,8 +209,16 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) { return; } hasRefetchedPublicRoom.current = true; - Report.openReport({reportID, introSelected, betas, hasReportActions: false, currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID}); - }, [isLoadingApp, allReports, introSelected, betas, session?.accountID]); + Report.openReport({ + reportID, + introSelected, + betas, + hasReportActions: false, + currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); + }, [isLoadingApp, allReports, introSelected, betas, session?.accountID, guidedSetupAndTourStatus?.isSelfTourViewed, guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow]); return null; } diff --git a/src/components/TestDrive/TestDriveDemo.tsx b/src/components/TestDrive/TestDriveDemo.tsx index 1da647771a80..990a243e4fdc 100644 --- a/src/components/TestDrive/TestDriveDemo.tsx +++ b/src/components/TestDrive/TestDriveDemo.tsx @@ -27,7 +27,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {delegateEmailSelector} from '@selectors/Account'; -import {hasSeenTourSelector} from '@selectors/Onboarding'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import TestDriveBanner from './TestDriveBanner'; @@ -54,13 +54,11 @@ function TestDriveDemo() { const parentReportAction = useParentReportAction(viewTourTaskReport); const isCurrentUserPolicyAdmin = useIsPaidPolicyAdmin(); - const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: hasSeenTourSelector, - }); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const hasCalledOpenReportRef = useRef(false); useEffect(() => { - if (hasSeenTour) { + if (guidedSetupAndTourStatus?.isSelfTourViewed) { return; } if (!viewTourTaskReport) { @@ -74,6 +72,8 @@ function TestDriveDemo() { betas, hasReportActions: hasConciergeReportActions, currentUserAccountID: currentUserPersonalDetails.accountID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, }); } return; @@ -93,7 +93,8 @@ function TestDriveDemo() { false, ); }, [ - hasSeenTour, + guidedSetupAndTourStatus?.isSelfTourViewed, + guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, hasConciergeReportActions, viewTourTaskReport, viewTourTaskParentReport, diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 7f2aa3abcb8a..cedb7cf1eb13 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -38,7 +38,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ReportAttributesDerivedValue} from '@src/types/onyx'; -import {hasSeenTourSelector} from '@selectors/Onboarding'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {useEffect, useRef} from 'react'; function initializePusher( @@ -62,7 +62,7 @@ function initializePusher( * * Extracted from AuthScreens to isolate useOnyx subscriptions: * - SESSION, NVP_INTRO_SELECTED, NVP_ACTIVE_POLICY_ID, - * NVP_ONBOARDING (tour selector), ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT (x2), + * NVP_ONBOARDING (guided-setup and tour status selector), ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT (x2), * IS_LOADING_APP */ function AuthScreensInitHandler() { @@ -78,7 +78,7 @@ function AuthScreensInitHandler() { const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [initialLastUpdateIDAppliedToClient] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT); - const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const lastWorkspaceNumber = useLastWorkspaceNumber(ownerEmail ?? undefined); @@ -170,7 +170,15 @@ function AuthScreensInitHandler() { } else if (SessionUtils.didUserLogInDuringSession()) { const reportID = getReportIDFromLink(initialURL ?? null); if (reportID && !isAuthenticatedAtStartup) { - Report.openReport({reportID, introSelected, betas, hasReportActions: false, currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID}); + Report.openReport({ + reportID, + introSelected, + betas, + hasReportActions: false, + currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed, + hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow, + }); // Don't want to call `openReport` again when logging out and then logging in setIsAuthenticatedAtStartup(true); } @@ -185,7 +193,7 @@ function AuthScreensInitHandler() { introSelected, currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD, activePolicy, - isSelfTourViewed, + guidedSetupAndTourStatus?.isSelfTourViewed, betas, hasActiveAdminPolicies, lastWorkspaceNumber, diff --git a/src/pages/inbox/ReportFetchHandler.tsx b/src/pages/inbox/ReportFetchHandler.tsx index a7b5627975c7..37b9ee90132d 100644 --- a/src/pages/inbox/ReportFetchHandler.tsx +++ b/src/pages/inbox/ReportFetchHandler.tsx @@ -58,6 +58,7 @@ import SCREENS from '@src/SCREENS'; import type {Transaction} from '@src/types/onyx'; import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; +import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding'; import {useEffect, useEffectEvent, useRef} from 'react'; type ReportScreenRoute = @@ -116,6 +117,7 @@ function ReportFetchHandler() { const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING); + const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); const [isLoadingReportData = true] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); const prevIsLoadingReportData = usePrevious(isLoadingReportData); @@ -153,6 +155,8 @@ function ReportFetchHandler() { const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false; const isOnboardingCompleted = onboarding?.hasCompletedGuidedSetupFlow ?? false; + const isSelfTourViewed = guidedSetupAndTourStatus?.isSelfTourViewed; + const hasCompletedGuidedSetupFlow = guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow; const isRegularOnboardingPending = !!introSelected && !introSelected.inviteType && isSupportedInviteOnboardingChoice(introSelected.choice) && !isOnboardingCompleted; const isPendingInviteOnboarding = isSupportedPendingInviteOnboarding(introSelected); const onboardingSignal = introSelected ? `${introSelected.choice ?? ''}:${introSelected.inviteType ?? ''}:${isInviteOnboardingComplete ? 'complete' : 'pending'}` : ''; @@ -199,7 +203,16 @@ function ReportFetchHandler() { return; } - openReport({reportID: reportIDFromRoute, introSelected, reportActionID: reportActionIDFromRoute, betas, hasReportActions, currentUserAccountID}); + openReport({ + reportID: reportIDFromRoute, + introSelected, + reportActionID: reportActionIDFromRoute, + betas, + hasReportActions, + currentUserAccountID, + isSelfTourViewed, + hasCompletedGuidedSetupFlow, + }); }); const createOneTransactionThread = useEffectEvent(() => { @@ -232,7 +245,7 @@ function ReportFetchHandler() { if (!shouldUseNarrowLayout || !isChatThread(report) || !isHiddenForCurrentUser(report) || isTransactionThreadView) { return; } - openReport({reportID, introSelected, betas, hasReportActions, currentUserAccountID}); + openReport({reportID, introSelected, betas, hasReportActions, currentUserAccountID, isSelfTourViewed, hasCompletedGuidedSetupFlow}); }); const joinPublicRoomIfNeeded = useEffectEvent(() => { @@ -240,7 +253,15 @@ function ReportFetchHandler() { if (!viewingPublicRoomReportID || viewingPublicRoomReportID === reportIDFromRoute) { return; } - openReport({reportID: viewingPublicRoomReportID, introSelected, betas, hasReportActions: hasViewingPublicRoomReportActions, currentUserAccountID}); + openReport({ + reportID: viewingPublicRoomReportID, + introSelected, + betas, + hasReportActions: hasViewingPublicRoomReportActions, + currentUserAccountID, + isSelfTourViewed, + hasCompletedGuidedSetupFlow, + }); }); // Effect order below matches the original declaration order in ReportScreen.tsx. diff --git a/tests/unit/DeepLinkHandlerTest.tsx b/tests/unit/DeepLinkHandlerTest.tsx index 77ab38769ca5..95c97680dbd2 100644 --- a/tests/unit/DeepLinkHandlerTest.tsx +++ b/tests/unit/DeepLinkHandlerTest.tsx @@ -69,6 +69,41 @@ describe('DeepLinkHandler', () => { expect(Report.openReport).toHaveBeenCalledWith(expect.objectContaining({reportID: PUBLIC_ROOM_ID})); }); + it('threads onboarding status from NVP_ONBOARDING into the refetch openReport (#66424)', async () => { + await act(async () => { + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS}, + [ONYXKEYS.IS_LOADING_APP]: true, + [ONYXKEYS.CONCIERGE_REPORT_ID]: '', + [ONYXKEYS.NVP_INTRO_SELECTED]: {}, + [ONYXKEYS.BETAS]: [], + // Deliberately mismatched flags so the assertion proves each field maps to its own source and they are not swapped. + // selfTourViewed feeds isSelfTourViewed (true), while hasCompletedGuidedSetupFlow passes through unchanged (false). + [ONYXKEYS.NVP_ONBOARDING]: {selfTourViewed: true, hasCompletedGuidedSetupFlow: false}, + }); + }); + + Linking.setInitialURL(`new-expensify://r/${PUBLIC_ROOM_ID}`); + + render(); + await waitForBatchedUpdatesWithAct(); + + await act(async () => { + await Onyx.merge(ONYXKEYS.IS_LOADING_APP, false); + }); + await waitForBatchedUpdatesWithAct(); + + // The combined NVP_ONBOARDING selector feeds both onboarding flags into openReport so guided-setup optimistic data + // is derived from real Onyx data instead of the deprecated module-level Onyx.connect fallback. + expect(Report.openReport).toHaveBeenCalledWith( + expect.objectContaining({ + reportID: PUBLIC_ROOM_ID, + isSelfTourViewed: true, + hasCompletedGuidedSetupFlow: false, + }), + ); + }); + it('does not refetch when the public room is already present in Onyx', async () => { await act(async () => { await Onyx.multiSet({