diff --git a/src/CONST.ts b/src/CONST.ts index 789d5aa90b028..bad481f5f343f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6991,6 +6991,8 @@ const CONST = { SCAN_TEST_TOOLTIP: 'scanTestTooltip', SCAN_TEST_TOOLTIP_MANAGER: 'scanTestTooltipManager', SCAN_TEST_CONFIRMATION: 'scanTestConfirmation', + GBR_RBR_CHAT: 'chatGBRRBR', + ACCOUNT_SWITCHER: 'accountSwitcher', EXPENSE_REPORTS_FILTER: 'expenseReportsFilter', }, CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 0f762cc4598f0..a0458cf2825c0 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -26,10 +26,17 @@ import * as Expensicons from './Icon/Expensicons'; import type {PopoverMenuItem} from './PopoverMenu'; import PopoverMenu from './PopoverMenu'; import {PressableWithFeedback} from './Pressable'; +import {useProductTrainingContext} from './ProductTrainingContext'; import Text from './Text'; import Tooltip from './Tooltip'; +import EducationalTooltip from './Tooltip/EducationalTooltip'; -function AccountSwitcher() { +type AccountSwitcherProps = { + /* Whether the screen is focused. Used to hide the product training tooltip */ + isScreenFocused: boolean; +}; + +function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const styles = useThemeStyles(); const theme = useTheme(); @@ -47,10 +54,42 @@ function AccountSwitcher() { const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const delegators = account?.delegatedAccess?.delegators ?? []; - const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; const canSwitchAccounts = delegators.length > 0 || isActingAsDelegate; const accountSwitcherPopoverStyle = canUseLeftHandBar ? styles.accountSwitcherPopoverWithLHB : styles.accountSwitcherPopover; + const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.ACCOUNT_SWITCHER, + isScreenFocused && canSwitchAccounts, + ); + + const onPressSwitcher = () => { + hideProductTrainingTooltip(); + setShouldShowDelegatorMenu(!shouldShowDelegatorMenu); + }; + + const TooltipToRender = shouldShowProductTrainingTooltip ? EducationalTooltip : Tooltip; + const tooltipProps = shouldShowProductTrainingTooltip + ? { + shouldRender: shouldShowProductTrainingTooltip, + renderTooltipContent: renderProductTrainingTooltip, + anchorAlignment: { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }, + shiftVertical: variables.accountSwitcherTooltipShiftVertical, + shiftHorizontal: variables.accountSwitcherTooltipShiftHorizontal, + wrapperStyle: styles.productTrainingTooltipWrapper, + onTooltipPress: onPressSwitcher, + } + : { + text: translate('delegate.copilotAccess'), + shiftVertical: 8, + shiftHorizontal: 8, + anchorAlignment: {horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM}, + shouldRender: canSwitchAccounts, + }; + const createBaseMenuItem = ( personalDetails: PersonalDetails | undefined, errors?: Errors, @@ -133,19 +172,12 @@ function AccountSwitcher() { return ( <> - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + { - setShouldShowDelegatorMenu(!shouldShowDelegatorMenu); - }} + onPress={onPressSwitcher} ref={buttonRef} interactive={canSwitchAccounts} pressDimmingValue={canSwitchAccounts ? undefined : 1} @@ -195,7 +227,8 @@ function AccountSwitcher() { - + + {!!canSwitchAccounts && ( { + if (!shouldShowProductTrainingTooltip) { + return undefined; + } + return data.find((reportID) => { + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + if (!itemFullReport) { + return false; + } + if (hasReportErrors(itemFullReport, itemReportActions)) { + return true; + } + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`]; + const itemParentReportAction = itemFullReport?.parentReportActionID ? itemParentReportActions?.[itemFullReport?.parentReportActionID] : undefined; + const hasGBR = requiresAttentionFromCurrentUser(itemFullReport, itemParentReportAction); + return hasGBR; + }); + }, [shouldShowProductTrainingTooltip, data, reportActions, reports]); + // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. const hasCalledOnLayout = React.useRef(false); @@ -181,6 +203,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio } const lastMessageTextFromReport = getLastMessageTextForReport(itemFullReport, lastActorDetails, itemPolicy, itemReportNameValuePairs); + const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; + return ( ); }, @@ -224,6 +249,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio transactionViolations, onLayoutItem, isOffline, + firstReportIDWithGBRorRBR, ], ); diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index dc88d5528704f..80223372ca807 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -12,6 +12,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {useSession} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import {useProductTrainingContext} from '@components/ProductTrainingContext'; +import type {ProductTrainingTooltipName} from '@components/ProductTrainingContext/TOOLTIPS'; import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; @@ -49,7 +50,17 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OptionRowLHNProps} from './types'; -function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style, onLayout = () => {}, hasDraftComment}: OptionRowLHNProps) { +function OptionRowLHN({ + reportID, + isFocused = false, + onSelectRow = () => {}, + optionItem, + viewMode = 'default', + style, + onLayout = () => {}, + hasDraftComment, + shouldShowRBRorGBRTooltip, +}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -57,10 +68,10 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const [isScreenFocused, setIsScreenFocused] = useState(false); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID}`); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID}`, {canBeMissing: true}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); + const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY, {canBeMissing: true}); const session = useSession(); const shouldShowWorkspaceChatTooltip = isPolicyExpenseChat(report) && !isThread(report) && activePolicyID === report?.policyID && session?.accountID === report?.ownerAccountID; const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); @@ -69,16 +80,25 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const isReportsSplitNavigatorLast = useRootNavigationState((state) => state?.routes?.at(-1)?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); - const {tooltipToRender, shouldShowTooltip} = useMemo(() => { + const {tooltipToRender, shouldShowTooltip, shouldTooltipBeLeftAligned} = useMemo(() => { // TODO: CONCIERGE_LHN_GBR tooltip will be replaced by a tooltip in the #admins room // https://github.com/Expensify/App/issues/57045#issuecomment-2701455668 - const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCIERGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; - const shouldShowTooltips = shouldShowWorkspaceChatTooltip || shouldShowGetStartedTooltip; + let tooltip: ProductTrainingTooltipName = shouldShowGetStartedTooltip + ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCIERGE_LHN_GBR + : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; + if (shouldShowRBRorGBRTooltip) { + tooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GBR_RBR_CHAT; + } + const shouldShowTooltips = shouldShowRBRorGBRTooltip || shouldShowWorkspaceChatTooltip || shouldShowGetStartedTooltip; const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast && !isFullscreenVisible; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return {tooltipToRender: tooltip, shouldShowTooltip: shouldShowTooltips && shouldTooltipBeVisible}; - }, [shouldShowGetStartedTooltip, shouldShowWorkspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast, isFullscreenVisible]); + return { + tooltipToRender: tooltip, + shouldShowTooltip: shouldShowTooltips && shouldTooltipBeVisible, + shouldTooltipBeLeftAligned: shouldShowWorkspaceChatTooltip && !shouldShowRBRorGBRTooltip && !shouldShowGetStartedTooltip, + }; + }, [shouldShowRBRorGBRTooltip, shouldShowGetStartedTooltip, shouldShowWorkspaceChatTooltip, isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast, isFullscreenVisible]); const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); @@ -195,11 +215,11 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldRender={shouldShowProductTrainingTooltip} renderTooltipContent={renderProductTrainingTooltip} anchorAlignment={{ - horizontal: shouldShowWorkspaceChatTooltip ? CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT : CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + horizontal: shouldTooltipBeLeftAligned ? CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT : CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, }} - shiftHorizontal={shouldShowWorkspaceChatTooltip ? variables.workspaceLHNTooltipShiftHorizontal : variables.gbrTooltipShiftHorizontal} - shiftVertical={shouldShowWorkspaceChatTooltip ? 0 : variables.gbrTooltipShiftVertical} + shiftHorizontal={shouldTooltipBeLeftAligned ? variables.workspaceLHNTooltipShiftHorizontal : variables.gbrTooltipShiftHorizontal} + shiftVertical={shouldTooltipBeLeftAligned ? 0 : variables.gbrTooltipShiftVertical} wrapperStyle={styles.productTrainingTooltipWrapper} onTooltipPress={onOptionPress} shouldHideOnScroll diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 6e294ff2d845e..7805ea1509d15 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -116,6 +116,9 @@ type OptionRowLHNDataProps = { /** Callback to execute when the OptionList lays out */ onLayout?: (event: LayoutChangeEvent) => void; + + /** Whether to show the educational tooltip for the GBR or RBR */ + shouldShowRBRorGBRTooltip: boolean; }; type OptionRowLHNProps = { @@ -141,6 +144,9 @@ type OptionRowLHNProps = { hasDraftComment: boolean; onLayout?: (event: LayoutChangeEvent) => void; + + /** Whether to show the educational tooltip on the GBR or RBR */ + shouldShowRBRorGBRTooltip: boolean; }; type RenderItemProps = {item: string}; diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts index 3d8a6c7eff94e..513888c957e97 100644 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -12,6 +12,8 @@ const { SCAN_TEST_TOOLTIP, SCAN_TEST_TOOLTIP_MANAGER, SCAN_TEST_CONFIRMATION, + GBR_RBR_CHAT, + ACCOUNT_SWITCHER, EXPENSE_REPORTS_FILTER, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; @@ -90,6 +92,29 @@ const TOOLTIPS: Record = { priority: 1800, shouldShow: ({isUserPolicyEmployee}) => isUserPolicyEmployee, }, + [GBR_RBR_CHAT]: { + content: [ + {text: 'productTrainingTooltip.GBRRBRChat.part1', isBold: false}, + {text: 'productTrainingTooltip.GBRRBRChat.part2', isBold: true}, + {text: 'productTrainingTooltip.GBRRBRChat.part3', isBold: false}, + {text: 'productTrainingTooltip.GBRRBRChat.part4', isBold: true}, + ], + onHideTooltip: () => dismissProductTraining(GBR_RBR_CHAT), + name: GBR_RBR_CHAT, + priority: 1900, + shouldShow: () => true, + }, + [ACCOUNT_SWITCHER]: { + content: [ + {text: 'productTrainingTooltip.accountSwitcher.part1', isBold: false}, + {text: 'productTrainingTooltip.accountSwitcher.part2', isBold: true}, + {text: 'productTrainingTooltip.accountSwitcher.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(ACCOUNT_SWITCHER), + name: ACCOUNT_SWITCHER, + priority: 1600, + shouldShow: () => true, + }, [EXPENSE_REPORTS_FILTER]: { content: [ {text: 'productTrainingTooltip.expenseReportsFilter.part1', isBold: false}, diff --git a/src/languages/en.ts b/src/languages/en.ts index df9994f6e67b1..ea3d06089f938 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6227,6 +6227,17 @@ const translations = { part3: '\nand more.', part4: ' Try it out!', }, + GBRRBRChat: { + part1: 'You’ll see 🟢 on ', + part2: 'actions to take', + part3: ',\nand 🔴 on ', + part4: 'errors to review.', + }, + accountSwitcher: { + part1: 'Access your ', + part2: 'Copilot accounts', + part3: ' here', + }, expenseReportsFilter: { part1: 'Welcome! Find all of your', part2: "\ncompany's reports", diff --git a/src/languages/es.ts b/src/languages/es.ts index d610b35efeaf4..187f183cf8d8d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6752,6 +6752,17 @@ const translations = { part3: '\ny más.', part4: ' ¡Pruébalo!', }, + GBRRBRChat: { + part1: 'Verás 🟢 en ', + part2: 'las acciones a realizar', + part3: '\ny 🔴 en ', + part4: 'los errores que debes revisar.', + }, + accountSwitcher: { + part1: 'Accede a tus ', + part2: 'cuentas copiloto', + part3: ' aquí', + }, expenseReportsFilter: { part1: '¡Bienvenido! Aquí encontrarás todos los', part2: '\ninformes de tu empresa', diff --git a/src/libs/Navigation/helpers/useRouteActive.ts b/src/libs/Navigation/helpers/useRouteActive.ts new file mode 100644 index 0000000000000..58d1cadb34bf4 --- /dev/null +++ b/src/libs/Navigation/helpers/useRouteActive.ts @@ -0,0 +1,16 @@ +import {findFocusedRoute, useNavigationState} from '@react-navigation/native'; +import useRootNavigationState from '@hooks/useRootNavigationState'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +function useIsAccountSettingsRouteActive(isNarrowLayout: boolean) { + const focusedRoute = useNavigationState(findFocusedRoute); + const navigationState = useRootNavigationState((x) => x); + + const isSettingsSplitNavigator = navigationState?.routes.at(-1)?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR; + const isAccountSettings = focusedRoute?.name === SCREENS.SETTINGS.ROOT; + + return isNarrowLayout ? isAccountSettings && isSettingsSplitNavigator : isSettingsSplitNavigator; +} + +export default useIsAccountSettingsRouteActive; diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index a747cac299bab..e8aad5ad9d9ea 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -34,6 +34,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {resetExitSurveyForm} from '@libs/actions/ExitSurvey'; import {checkIfFeedConnectionIsBroken} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; +import useIsAccountSettingsRouteActive from '@libs/Navigation/helpers/useRouteActive'; import Navigation from '@libs/Navigation/Navigation'; import {getFreeTrialText, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils'; import {getProfilePageBrickRoadIndicator} from '@libs/UserUtils'; @@ -91,7 +92,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); - const [allCards] = useOnyx(`${ONYXKEYS.CARD_LIST}`, {canBeMissing: true}); + const [allCards] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayout(); const network = useNetwork(); @@ -118,6 +119,8 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const freeTrialText = getFreeTrialText(policies); const shouldOpenBookACall = tryNewDot?.classicRedirect?.dismissed === false; + const isScreenFocused = useIsAccountSettingsRouteActive(shouldUseNarrowLayout); + useEffect(() => { openInitialSettingsPage(); confirmReadyToOpenApp(); @@ -386,7 +389,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr ) : ( - +