diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ced14aca92ca..73594a466c46 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5870,6 +5870,11 @@ const CONST = { ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers}, ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize}, + ONBOARDING_RHP_VARIANT: { + RHP_CONCIERGE_DM: 'rhpConciergeDm', + RHP_ADMINS_ROOM: 'rhpAdminsRoom', + CONTROL: 'control', + }, ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE: 'What would you like to do with this expense?', ONBOARDING_ACCOUNTING_MAPPING, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 603a3507b0b0..47dbc171a2b7 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -591,6 +591,9 @@ const ONYXKEYS = { /** Stores the user's app review prompt state and response */ NVP_APP_REVIEW: 'nvp_appReview', + /** Stores the onboarding RHP variant for A/B/C testing */ + NVP_ONBOARDING_RHP_VARIANT: 'nvp_onboardingRHPVariant', + /** Information about vacation delegate */ NVP_PRIVATE_VACATION_DELEGATE: 'nvp_private_vacationDelegate', @@ -1355,6 +1358,7 @@ type OnyxValuesMapping = { [ONYXKEYS.BILLING_RECEIPT_DETAILS]: OnyxTypes.BillingReceiptDetails; [ONYXKEYS.NVP_SIDE_PANEL]: OnyxTypes.SidePanel; [ONYXKEYS.NVP_APP_REVIEW]: OnyxTypes.AppReview; + [ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT]: OnyxTypes.OnboardingRHPVariant; [ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION]: boolean; [ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE]: OnyxTypes.VacationDelegate; [ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft; diff --git a/src/components/SidePanel/RHPVariantTest/index.native.ts b/src/components/SidePanel/RHPVariantTest/index.native.ts new file mode 100644 index 000000000000..466080a1b07d --- /dev/null +++ b/src/components/SidePanel/RHPVariantTest/index.native.ts @@ -0,0 +1,13 @@ +import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types'; + +/** + * Side Panel is not supported on native platforms, so we always return false. + */ +const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => false; + +/** + * No-op on native platforms since Side Panel is not supported. + */ +const handleRHPVariantNavigation: HandleRHPVariantNavigation = () => {}; + +export {shouldOpenRHPVariant, handleRHPVariantNavigation}; diff --git a/src/components/SidePanel/RHPVariantTest/index.ts b/src/components/SidePanel/RHPVariantTest/index.ts new file mode 100644 index 000000000000..b301a5c8e89b --- /dev/null +++ b/src/components/SidePanel/RHPVariantTest/index.ts @@ -0,0 +1,55 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import SidePanelActions from '@libs/actions/SidePanel'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {OnboardingRHPVariant} from '@src/types/onyx'; +import type {HandleRHPVariantNavigation, ShouldOpenRHPVariant} from './types'; + +let onboardingRHPVariant: OnyxEntry; +let onboardingCompanySize: OnyxEntry; + +// We use Onyx.connectWithoutView because we do not use this in React components and this logic is not tied directly to the UI. +Onyx.connectWithoutView({ + key: ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, + callback: (value) => { + onboardingRHPVariant = value; + }, +}); + +Onyx.connectWithoutView({ + key: ONYXKEYS.ONBOARDING_COMPANY_SIZE, + callback: (value) => { + onboardingCompanySize = value; + }, +}); + +/** + * Determines if the user should be navigated to the RHP variant side panel after onboarding. + * The RHP variant is only shown to micro companies that are part of the RHP experiment. + */ +const shouldOpenRHPVariant: ShouldOpenRHPVariant = () => { + const isMicroCompany = onboardingCompanySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO; + const isRHPConciergeDM = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_CONCIERGE_DM; + const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; + + return isMicroCompany && (isRHPConciergeDM || isRHPAdminsRoom); +}; + +/** + * Handles navigation for RHP experiment: + * - Control: navigate to the last accessed report on small screens, do not open side panel + * - RHP Concierge DM: navigate to the workspace overview and open the side panel with the Concierge DM + * - RHP Admins Room: navigate to the workspace overview and open the side panel with the Admins Room + */ +const handleRHPVariantNavigation: HandleRHPVariantNavigation = (onboardingPolicyID) => { + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(onboardingPolicyID)); + SidePanelActions.openSidePanel(true); + Navigation.isNavigationReady().then(() => { + Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); + }); +}; + +export {shouldOpenRHPVariant, handleRHPVariantNavigation}; diff --git a/src/components/SidePanel/RHPVariantTest/types.ts b/src/components/SidePanel/RHPVariantTest/types.ts new file mode 100644 index 000000000000..7efd441c0526 --- /dev/null +++ b/src/components/SidePanel/RHPVariantTest/types.ts @@ -0,0 +1,4 @@ +type ShouldOpenRHPVariant = () => boolean; +type HandleRHPVariantNavigation = (onboardingPolicyID?: string) => void; + +export type {ShouldOpenRHPVariant, HandleRHPVariantNavigation}; diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx index 1a50aa54aac0..3318bc08906f 100644 --- a/src/components/SidePanel/SidePanelContextProvider.tsx +++ b/src/components/SidePanel/SidePanelContextProvider.tsx @@ -3,14 +3,18 @@ import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} // Import Animated directly from 'react-native' as animations are used with navigation. // eslint-disable-next-line no-restricted-imports import {Animated} from 'react-native'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSidePanelDisplayStatus from '@hooks/useSidePanelDisplayStatus'; import useWindowDimensions from '@hooks/useWindowDimensions'; import SidePanelActions from '@libs/actions/SidePanel'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {emailSelector} from '@src/selectors/Session'; import type {SidePanel} from '@src/types/onyx'; type SidePanelContextProps = { @@ -25,6 +29,7 @@ type SidePanelContextProps = { openSidePanel: () => void; closeSidePanel: () => void; sidePanelNVP?: SidePanel; + reportID?: string; }; const SidePanelContext = createContext({ @@ -56,6 +61,28 @@ function SidePanelContextProvider({children}: PropsWithChildren) { const sidePanelOffset = useRef(new Animated.Value(shouldApplySidePanelOffset ? variables.sidePanelWidth : 0)); const sidePanelTranslateX = useRef(new Animated.Value(shouldHideSidePanel ? sidePanelWidth : 0)); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true}); + const [onboardingRHPVariant] = useOnyx(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, {canBeMissing: true}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, { + canBeMissing: true, + selector: emailSelector, + }); + + const reportID = useMemo(() => { + const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM; + const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail); + const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? ''); + const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString(); + + if (isRHPAdminsRoom && isUserAdmin && isPolicyActive && adminsChatReportID) { + return adminsChatReportID; + } + + return conciergeReportID; + }, [onboardingRHPVariant, activePolicy, sessionEmail, conciergeReportID]); + useEffect(() => { setIsSidePanelTransitionEnded(false); Animated.parallel([ @@ -103,6 +130,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) { openSidePanel: () => SidePanelActions.openSidePanel(!isExtraLargeScreenWidth), closeSidePanel, sidePanelNVP, + reportID, }), [ closeSidePanel, @@ -114,6 +142,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) { shouldHideSidePanelBackdrop, shouldHideToolTip, sidePanelNVP, + reportID, ], ); diff --git a/src/components/SidePanel/Concierge/index.tsx b/src/components/SidePanel/SidePanelReport/index.tsx similarity index 61% rename from src/components/SidePanel/Concierge/index.tsx rename to src/components/SidePanel/SidePanelReport/index.tsx index d9fc9eb8f7b7..ba6e527107ac 100644 --- a/src/components/SidePanel/Concierge/index.tsx +++ b/src/components/SidePanel/SidePanelReport/index.tsx @@ -1,22 +1,17 @@ import {NavigationRouteContext} from '@react-navigation/native'; import React from 'react'; -import useOnyx from '@hooks/useOnyx'; import type {ExtraContentProps, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList} from '@libs/Navigation/types'; import ReportScreen from '@pages/home/ReportScreen'; -import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; -const CONCIERGE_REPORT_KEY = 'Report-Concierge-Key'; +type SidePanelReportProps = Pick & { + reportID: string; +}; -function Concierge({navigation}: Pick) { - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID, {canBeMissing: true}); +function SidePanelReport({navigation, reportID}: SidePanelReportProps) { // eslint-disable-next-line react/jsx-no-constructed-context-values - const route = !!conciergeReportID && ({name: SCREENS.REPORT, params: {reportID: conciergeReportID}, key: CONCIERGE_REPORT_KEY} as const); - - if (!route) { - return null; - } + const route = {name: SCREENS.REPORT, params: {reportID}, key: `Report-SidePanel-${reportID}`} as const; return ( @@ -29,4 +24,4 @@ function Concierge({navigation}: Pick) { ); } -export default Concierge; +export default SidePanelReport; diff --git a/src/components/SidePanel/index.tsx b/src/components/SidePanel/index.tsx index 168a2035999f..677f3c9e6686 100644 --- a/src/components/SidePanel/index.tsx +++ b/src/components/SidePanel/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; import useSidePanel from '@hooks/useSidePanel'; import type {ExtraContentProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import Concierge from './Concierge'; import SidePanelModal from './SidePanelModal'; +import SidePanelReport from './SidePanelReport'; import useSyncSidePanelWithHistory from './useSyncSidePanelWithHistory'; function SidePanel({navigation}: Pick) { - const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel} = useSidePanel(); + const {sidePanelNVP, isSidePanelTransitionEnded, shouldHideSidePanel, sidePanelTranslateX, shouldHideSidePanelBackdrop, closeSidePanel, reportID} = useSidePanel(); // Hide side panel once animation ends // This hook synchronizes the side panel visibility with the browser history when it is displayed as RHP. @@ -15,7 +15,7 @@ function SidePanel({navigation}: Pick) { useSyncSidePanelWithHistory(); // Side panel can't be displayed if NVP is undefined - if (!sidePanelNVP) { + if (!sidePanelNVP || !reportID) { return null; } @@ -30,7 +30,10 @@ function SidePanel({navigation}: Pick) { closeSidePanel={closeSidePanel} shouldHideSidePanelBackdrop={shouldHideSidePanelBackdrop} > - + ); } diff --git a/src/components/SidePanel/isSidePanelReportSupported/index.native.ts b/src/components/SidePanel/isSidePanelReportSupported/index.native.ts new file mode 100644 index 000000000000..dde4cf7b9bc8 --- /dev/null +++ b/src/components/SidePanel/isSidePanelReportSupported/index.native.ts @@ -0,0 +1,5 @@ +import type IsSidePanelReportSupported from './types'; + +const isSidePanelReportSupported: IsSidePanelReportSupported = false; + +export default isSidePanelReportSupported; diff --git a/src/components/SidePanel/isSidePanelReportSupported/index.ts b/src/components/SidePanel/isSidePanelReportSupported/index.ts new file mode 100644 index 000000000000..5a807f44d232 --- /dev/null +++ b/src/components/SidePanel/isSidePanelReportSupported/index.ts @@ -0,0 +1,5 @@ +import type IsSidePanelReportSupported from './types'; + +const isSidePanelReportSupported: IsSidePanelReportSupported = true; + +export default isSidePanelReportSupported; diff --git a/src/components/SidePanel/isSidePanelReportSupported/types.ts b/src/components/SidePanel/isSidePanelReportSupported/types.ts new file mode 100644 index 000000000000..c037d7d6a756 --- /dev/null +++ b/src/components/SidePanel/isSidePanelReportSupported/types.ts @@ -0,0 +1,3 @@ +type IsSidePanelReportSupported = boolean; + +export default IsSidePanelReportSupported; diff --git a/src/components/TestDrive/Modal/AdminTestDriveModal.tsx b/src/components/TestDrive/Modal/AdminTestDriveModal.tsx index 7c04073865d0..559ba3a1310e 100644 --- a/src/components/TestDrive/Modal/AdminTestDriveModal.tsx +++ b/src/components/TestDrive/Modal/AdminTestDriveModal.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {InteractionManager} from 'react-native'; +import {shouldOpenRHPVariant} from '@components/SidePanel/RHPVariantTest'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import Log from '@libs/Log'; @@ -27,6 +28,11 @@ function AdminTestDriveModal() { Log.hmmm('[AdminTestDriveModal] Skip test drive function called'); Navigation.dismissModal(); + if (shouldOpenRHPVariant()) { + Log.hmmm('[AdminTestDriveModal] User was redirected to Workspace Editor, skipping navigation to admin room'); + return; + } + Log.hmmm('[AdminTestDriveModal] Running after interactions'); Navigation.setNavigationActionToMicrotaskQueue(() => { if (!isAdminRoom(onboardingReport)) { diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 996fa7e9479f..a2ee05fab0fa 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -217,11 +217,10 @@ function makeRequestWithSideEffects( } /** - * Ensure all write requests on the sequential queue have finished responding before running read requests. - * Responses from read requests can overwrite the optimistic data inserted by - * write requests that use the same Onyx keys and haven't responded yet. + * Ensure all write requests on the sequential queue have finished responding before running a command. + * Responses from requests can overwrite the optimistic data inserted by write requests that use the same Onyx keys and haven't responded yet. */ -function waitForWrites(command: TCommand) { +function waitForWrites(command: TCommand) { if (getPersistedRequestsLength() > 0) { Log.info(`[API] '${command}' is waiting on ${getPersistedRequestsLength()} write commands`); } diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 114a0213a96c..b808473b64bc 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1284,6 +1284,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { CALCULATE_BILL_NEW_DOT: 'CalculateBillNewDot', ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms', + COMPLETE_GUIDED_SETUP: 'CompleteGuidedSetup', } as const; type SideEffectRequestCommand = ValueOf; @@ -1312,6 +1313,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.CALCULATE_BILL_NEW_DOT]: null; [SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: Parameters.AcceptSpotnanaTermsParams; [SIDE_EFFECT_REQUEST_COMMANDS.GET_SCIM_TOKEN]: Parameters.GetScimTokenParams; + [SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_GUIDED_SETUP]: Parameters.CompleteGuidedSetupParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 469cf74c80da..51d64fbd2bd1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -10,6 +10,7 @@ import type {Emoji} from '@assets/emojis/types'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {waitForWrites} from '@libs/API'; import * as API from '@libs/API'; import type { AddCommentOrAttachmentParams, @@ -58,7 +59,7 @@ import type { } from '@libs/API/parameters'; import type ExportReportCSVParams from '@libs/API/parameters/ExportReportCSVParams'; import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams'; -import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; import * as Browser from '@libs/Browser'; import * as CollectionUtils from '@libs/CollectionUtils'; @@ -4388,22 +4389,7 @@ function getReportPrivateNote(reportID: string | undefined) { API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData}); } -function completeOnboarding({ - engagementChoice, - onboardingMessage, - firstName = '', - lastName = '', - adminsChatReportID, - onboardingPolicyID, - paymentSelected, - companySize, - userReportedIntegration, - wasInvited, - selectedInterestedFeatures = [], - shouldSkipTestDriveModal, - isInvitedAccountant, - onboardingPurposeSelected, -}: { +type CompleteOnboardingProps = { engagementChoice: OnboardingPurpose; onboardingMessage: OnboardingMessage; firstName?: string; @@ -4418,7 +4404,26 @@ function completeOnboarding({ shouldSkipTestDriveModal?: boolean; isInvitedAccountant?: boolean; onboardingPurposeSelected?: OnboardingPurpose; -}) { + shouldWaitForRHPVariantInitialization?: boolean; +}; + +async function completeOnboarding({ + engagementChoice, + onboardingMessage, + firstName = '', + lastName = '', + adminsChatReportID, + onboardingPolicyID, + paymentSelected, + companySize, + userReportedIntegration, + wasInvited, + selectedInterestedFeatures = [], + shouldSkipTestDriveModal, + isInvitedAccountant, + onboardingPurposeSelected, + shouldWaitForRHPVariantInitialization = false, +}: CompleteOnboardingProps) { const onboardingData = prepareOnboardingOnyxData({ introSelected, engagementChoice, @@ -4473,7 +4478,18 @@ function completeOnboarding({ }); } - API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData, failureData}); + if (shouldWaitForRHPVariantInitialization) { + // Wait for the workspace to be created before completing the guided setup + await waitForWrites(SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_GUIDED_SETUP); + + // We need to access the nvp_onboardingRHPVariant directly from the response to redirect the user to the correct page + // eslint-disable-next-line rulesdir/no-api-side-effects-method + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData, failureData}); + } + + // API calls are not chained in this case + // eslint-disable-next-line rulesdir/no-multiple-api-calls + return API.write(WRITE_COMMANDS.COMPLETE_GUIDED_SETUP, parameters, {optimisticData, successData, failureData}); } /** Loads necessary data for rendering the RoomMembersPage */ diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index 311277ee8275..d28df7135623 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -1,37 +1,59 @@ +import {handleRHPVariantNavigation, shouldOpenRHPVariant} from '@components/SidePanel/RHPVariantTest'; import ROUTES from '@src/ROUTES'; import {setDisableDismissOnEscape} from './actions/Modal'; import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; import {findLastAccessedReport, isConciergeChatReport, isSelfDM} from './ReportUtils'; -const navigateAfterOnboarding = ( +/** + * Determines the report ID to navigate to after onboarding for control variant or ineligible users. + * On large screens, navigates to the admins chat if available. On small screens, finds the last + * accessed report while avoiding self DM, Concierge chat, and reports from the onboarding policy. + */ +function getReportIDAfterOnboarding( isSmallScreenWidth: boolean, canUseDefaultRooms: boolean | undefined, onboardingPolicyID?: string, onboardingAdminsChatReportID?: string, shouldPreventOpenAdminRoom = false, -) => { - setDisableDismissOnEscape(false); - - let reportID: string | undefined; - +): string | undefined { // When hasCompletedGuidedSetupFlow is true, OnboardingModalNavigator in AuthScreen is removed from the navigation stack. // On small screens, this removal redirects navigation to HOME. Dismissing the modal doesn't work properly, // so we need to specifically navigate to the last accessed report. if (!isSmallScreenWidth) { if (onboardingAdminsChatReportID && !shouldPreventOpenAdminRoom) { - reportID = onboardingAdminsChatReportID; - } - } else { - const lastAccessedReport = findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom() && !shouldPreventOpenAdminRoom); - const lastAccessedReportID = lastAccessedReport?.reportID; - // When the user goes through the onboarding flow, a workspace can be created if the user selects specific options. The user should be taken to the #admins room for that workspace because it is the most natural place for them to start their experience in the app. - // The user should never go to the self DM or the Concierge chat if a workspace was created during the onboarding flow. - if (lastAccessedReportID && lastAccessedReport.policyID !== onboardingPolicyID && !isConciergeChatReport(lastAccessedReport) && !isSelfDM(lastAccessedReport)) { - reportID = lastAccessedReportID; + return onboardingAdminsChatReportID; } + return undefined; + } + + const lastAccessedReport = findLastAccessedReport(!canUseDefaultRooms, shouldOpenOnAdminRoom() && !shouldPreventOpenAdminRoom); + const lastAccessedReportID = lastAccessedReport?.reportID; + + // When the user goes through the onboarding flow, a workspace can be created if the user selects specific options. The user should be taken to the #admins room for that workspace because it is the most natural place for them to start their experience in the app. + // The user should never go to the self DM or the Concierge chat if a workspace was created during the onboarding flow. + if (lastAccessedReportID && lastAccessedReport.policyID !== onboardingPolicyID && !isConciergeChatReport(lastAccessedReport) && !isSelfDM(lastAccessedReport)) { + return lastAccessedReportID; + } + + return undefined; +} + +function navigateAfterOnboarding( + isSmallScreenWidth: boolean, + canUseDefaultRooms: boolean | undefined, + onboardingPolicyID?: string, + onboardingAdminsChatReportID?: string, + shouldPreventOpenAdminRoom = false, +) { + setDisableDismissOnEscape(false); + + if (shouldOpenRHPVariant()) { + handleRHPVariantNavigation(onboardingPolicyID); + return; } + const reportID = getReportIDAfterOnboarding(isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, onboardingAdminsChatReportID, shouldPreventOpenAdminRoom); if (reportID) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); } @@ -48,19 +70,19 @@ const navigateAfterOnboarding = ( Navigation.isNavigationReady().then(() => { Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); }); -}; +} -const navigateAfterOnboardingWithMicrotaskQueue = ( +function navigateAfterOnboardingWithMicrotaskQueue( isSmallScreenWidth: boolean, canUseDefaultRooms: boolean | undefined, onboardingPolicyID?: string, onboardingAdminsChatReportID?: string, shouldPreventOpenAdminRoom = false, -) => { +) { Navigation.dismissModal(); Navigation.setNavigationActionToMicrotaskQueue(() => { navigateAfterOnboarding(isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, onboardingAdminsChatReportID, shouldPreventOpenAdminRoom); }); -}; +} export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue}; diff --git a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx index 16a81d11f902..49121eef6597 100644 --- a/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx +++ b/src/pages/OnboardingInterestedFeatures/BaseOnboardingInterestedFeatures.tsx @@ -9,6 +9,7 @@ import {PressableWithoutFeedback} from '@components/Pressable'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import isSidePanelReportSupported from '@components/SidePanel/isSidePanelReportSupported'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -22,6 +23,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {createWorkspace, generatePolicyID} from '@libs/actions/Policy/Policy'; import {completeOnboarding} from '@libs/actions/Report'; import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@libs/actions/Welcome'; +import Log from '@libs/Log'; import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; @@ -55,6 +57,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin const paidGroupPolicy = Object.values(allPolicies ?? {}).find((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)); const {isOffline} = useNetwork(); const [width, setWidth] = useState(0); + const [isLoading, setIsLoading] = useState(false); const features: Feature[] = useMemo(() => { return [ @@ -152,76 +155,85 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin setOnboardingPolicyID(paidGroupPolicy.id); }, [paidGroupPolicy, onboardingPolicyID]); - const handleContinue = useCallback(() => { + const handleContinue = useCallback(async () => { if (!onboardingPurposeSelected || !onboardingCompanySize) { return; } - const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; - const newUserReportedIntegration = selectedFeatures.some((feature) => feature === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED) ? userReportedIntegration : undefined; - const featuresMap = features.map((feature) => ({ - ...feature, - enabled: selectedFeatures.includes(feature.id), - })); + try { + setIsLoading(true); - // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. - // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. - const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? createWorkspace({ - policyOwnerEmail: undefined, - makeMeAdmin: true, - policyName: '', - policyID: generatePolicyID(), - engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, - currency: currentUserPersonalDetails?.localCurrencyCode ?? '', - file: undefined, - shouldAddOnboardingTasks: false, - companySize: onboardingCompanySize, - userReportedIntegration: newUserReportedIntegration, - featuresMap, - introSelected, - activePolicyID, - currentUserAccountIDParam: currentUserPersonalDetails.accountID, - currentUserEmailParam: currentUserPersonalDetails.email ?? '', - shouldAddGuideWelcomeMessage: false, - }) - : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; + const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; + const newUserReportedIntegration = selectedFeatures.some((feature) => feature === CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED) ? userReportedIntegration : undefined; + const featuresMap = features.map((feature) => ({ + ...feature, + enabled: selectedFeatures.includes(feature.id), + })); - if (shouldCreateWorkspace) { - setOnboardingAdminsChatReportID(adminsChatReportID); - setOnboardingPolicyID(policyID); - } + // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once. + // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. + const {adminsChatReportID, policyID} = shouldCreateWorkspace + ? createWorkspace({ + policyOwnerEmail: undefined, + makeMeAdmin: true, + policyName: '', + policyID: generatePolicyID(), + engagementChoice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + currency: currentUserPersonalDetails?.localCurrencyCode ?? '', + file: undefined, + shouldAddOnboardingTasks: false, + companySize: onboardingCompanySize, + userReportedIntegration: newUserReportedIntegration, + featuresMap, + introSelected, + activePolicyID, + currentUserAccountIDParam: currentUserPersonalDetails.accountID, + currentUserEmailParam: currentUserPersonalDetails.email ?? '', + shouldAddGuideWelcomeMessage: false, + }) + : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; - completeOnboarding({ - engagementChoice: onboardingPurposeSelected, - onboardingMessage: onboardingMessages[onboardingPurposeSelected], - adminsChatReportID, - onboardingPolicyID: policyID, - companySize: onboardingCompanySize, - userReportedIntegration: newUserReportedIntegration, - firstName: currentUserPersonalDetails?.firstName, - lastName: currentUserPersonalDetails?.lastName, - selectedInterestedFeatures: featuresMap.filter((feature) => feature.enabled).map((feature) => feature.id), - shouldSkipTestDriveModal: !!policyID && !adminsChatReportID, - }); + if (shouldCreateWorkspace) { + setOnboardingAdminsChatReportID(adminsChatReportID); + setOnboardingPolicyID(policyID); + } - // Avoid creating new WS because onboardingPolicyID is cleared before unmounting - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - setOnboardingAdminsChatReportID(); - setOnboardingPolicyID(); - }); + await completeOnboarding({ + engagementChoice: onboardingPurposeSelected, + onboardingMessage: onboardingMessages[onboardingPurposeSelected], + adminsChatReportID, + onboardingPolicyID: policyID, + companySize: onboardingCompanySize, + userReportedIntegration: newUserReportedIntegration, + firstName: currentUserPersonalDetails?.firstName, + lastName: currentUserPersonalDetails?.lastName, + selectedInterestedFeatures: featuresMap.filter((feature) => feature.enabled).map((feature) => feature.id), + shouldSkipTestDriveModal: !!policyID && !adminsChatReportID, + shouldWaitForRHPVariantInitialization: isSidePanelReportSupported, + }); + + // Avoid creating new WS because onboardingPolicyID is cleared before unmounting + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + setOnboardingAdminsChatReportID(); + setOnboardingPolicyID(); + }); - // We need to wait the policy is created before navigating out the onboarding flow - navigateAfterOnboardingWithMicrotaskQueue( - isSmallScreenWidth, - isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), - policyID, - adminsChatReportID, - // Onboarding tasks would show in Concierge instead of admins room for testing accounts, we should open where onboarding tasks are located - // See https://github.com/Expensify/App/issues/57167 for more details - (session?.email ?? '').includes('+'), - ); + // We need to wait the policy is created before navigating out the onboarding flow + navigateAfterOnboardingWithMicrotaskQueue( + isSmallScreenWidth, + isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), + policyID, + adminsChatReportID, + // Onboarding tasks would show in Concierge instead of admins room for testing accounts, we should open where onboarding tasks are located + // See https://github.com/Expensify/App/issues/57167 for more details + (session?.email ?? '').includes('+'), + ); + } catch (error) { + Log.warn('[BaseOnboardingInterestedFeatures] Error completing onboarding', {error}); + } finally { + setIsLoading(false); + } }, [ isBetaEnabled, isSmallScreenWidth, @@ -366,6 +378,7 @@ function BaseOnboardingInterestedFeatures({shouldUseNativeStyles}: BaseOnboardin text={translate('common.continue')} onPress={handleContinue} isDisabled={isOffline} + isLoading={isLoading} pressOnEnter /> diff --git a/src/types/onyx/OnboardingRHPVariant.ts b/src/types/onyx/OnboardingRHPVariant.ts new file mode 100644 index 000000000000..271e1977e737 --- /dev/null +++ b/src/types/onyx/OnboardingRHPVariant.ts @@ -0,0 +1,9 @@ +/** + * The variant of the onboarding RHP for A/B/C testing + * @description 'control' - The variant with the Concierge DM + * @description 'rhpConciergeDm' - Admin of workspace with Concierge DM + * @description 'rhpAdminsRoom' - Admin of workspace with the admins room + */ +type OnboardingRHPVariant = 'rhpConciergeDm' | 'rhpAdminsRoom' | 'control'; + +export default OnboardingRHPVariant; diff --git a/src/types/onyx/SidePanel.tsx b/src/types/onyx/SidePanel.tsx index 6c04cf14543c..915e2ed7c48c 100644 --- a/src/types/onyx/SidePanel.tsx +++ b/src/types/onyx/SidePanel.tsx @@ -1,9 +1,4 @@ -type SidePanelContent = 'help'; - type SidePanel = { - /** The content of the Side Panel */ - content?: SidePanelContent; - /** Whether the Side Panel is open on large screens */ open: boolean; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index a6fd3fbfcdae..81e89f0df9e3 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -64,6 +64,7 @@ import type Modal from './Modal'; import type Network from './Network'; import type NewGroupChatDraft from './NewGroupChatDraft'; import type Onboarding from './Onboarding'; +import type OnboardingRHPVariant from './OnboardingRHPVariant'; import type OnyxInputOrEntry from './OnyxInputOrEntry'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import type {DecisionName, OriginalMessageIOU} from './OriginalMessage'; @@ -292,6 +293,7 @@ export type { ImportedSpreadsheetMemberData, Onboarding, OnboardingPurpose, + OnboardingRHPVariant, ValidateMagicCodeAction, ShareTempFile, CorpayFields,