diff --git a/src/App.tsx b/src/App.tsx index 52904e0a06c4..cc824b78fa4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; +import {ProductTrainingContextProvider} from './components/ProductTrainingContext'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; import {SearchRouterContextProvider} from './components/Search/SearchRouter/SearchRouterContext'; @@ -95,6 +96,7 @@ function App({url}: AppProps) { VideoPopoverMenuContextProvider, KeyboardProvider, SearchRouterContextProvider, + ProductTrainingContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 740ee2206876..cf9e5d8a2886 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6444,6 +6444,17 @@ const CONST = { }, MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal', + + PRODUCT_TRAINING_TOOLTIP_NAMES: { + CONCEIRGE_LHN_GBR: 'conciergeLHNGBR', + RENAME_SAVED_SEARCH: 'renameSavedSearch', + QUICK_ACTION_BUTTON: 'quickActionButton', + WORKSAPCE_CHAT_CREATE: 'workspaceChatCreate', + SEARCH_FILTER_BUTTON_TOOLTIP: 'filterButtonTooltip', + BOTTOM_NAV_INBOX_TOOLTIP: 'bottomNavInboxTooltip', + LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip', + GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 86d4c90d67ed..026ab2310622 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -117,9 +117,6 @@ const ONYXKEYS = { /** NVP keys */ - /** Boolean flag only true when first set */ - NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', - /** This NVP contains list of at most 5 recent attendees */ NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees', @@ -222,18 +219,9 @@ const ONYXKEYS = { /** The end date (epoch timestamp) of the workspace owner’s grace period after the free trial ends. */ NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END: 'nvp_private_billingGracePeriodEnd', - /** The NVP containing all information related to educational tooltip in workspace chat */ - NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', - /** The NVP containing the target url to navigate to when deleting a transaction */ NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL', - /** Whether to show save search rename tooltip */ - SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip', - - /** Whether to hide gbr tooltip */ - NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip', - /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -888,7 +876,6 @@ type OnyxCollectionValuesMapping = { type OnyxValuesMapping = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; - [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; [ONYXKEYS.NVP_ONBOARDING]: Onboarding; @@ -1030,9 +1017,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; - [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; [ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined; - [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; @@ -1040,7 +1025,6 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; - [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 3c831301db8b..8ba640956bf3 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -3,12 +3,20 @@ import React, {forwardRef, useEffect, useRef} from 'react'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Role, Text, View} from 'react-native'; import {Platform} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; +import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import {PressableWithoutFeedback} from './Pressable'; +import {useProductTrainingContext} from './ProductTrainingContext'; +import EducationalTooltip from './Tooltip/EducationalTooltip'; const AnimatedPath = Animated.createAnimatedComponent(Path); AnimatedPath.displayName = 'AnimatedPath'; @@ -56,6 +64,15 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const fabPressable = useRef(null); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const platform = getPlatform(); + const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB; + const isFocused = useBottomTabIsFocused(); + const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED, {initialValue: false}); + const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP, + isFocused && isSidebarLoaded, + ); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -97,32 +114,45 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } + {}} - role={role} - shouldUseHapticsOnLongPress={false} + shouldUseOverlay + shiftHorizontal={isNarrowScreenOnWeb ? 0 : variables.fabTooltipShiftHorizontal} + renderTooltipContent={renderProductTrainingTooltip} + wrapperStyle={styles.productTrainingTooltipWrapper} + onHideTooltip={hideProductTrainingTooltip} > - - - - - - + { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } + }} + style={[styles.h100, styles.bottomTabBarItem]} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} + > + + + + + + + ); } diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index c423d3101d92..efdd9659c845 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -11,6 +11,7 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {useSession} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; +import {useProductTrainingContext} from '@components/ProductTrainingContext'; import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; @@ -22,7 +23,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; -import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; @@ -32,7 +32,6 @@ import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportA import FreeTrial from '@pages/settings/Subscription/FreeTrial'; import variables from '@styles/variables'; import Timing from '@userActions/Timing'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -48,18 +47,21 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); - const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: hasCompletedGuidedSetupFlowSelector, - }); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const isActiveWorkspaceChat = ReportUtils.isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat && activePolicyID === report?.policyID; const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const session = useSession(); - - // Guides are assigned for the MANAGE_TEAM onboarding action, except for emails that have a '+'. const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); - const shouldShowToooltipOnThisReport = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); - const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, {initialValue: true}); + const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); + + const {tooltipToRender, shouldShowTooltip} = useMemo(() => { + const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return {tooltipToRender: tooltip, shouldShowTooltip: shouldUseNarrowLayout ? isScreenFocused : true}; + }, [shouldShowGetStartedTooltip, isScreenFocused, shouldUseNarrowLayout]); + const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -72,30 +74,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti }, []), ); - const renderGBRTooltip = useCallback( - () => ( - - - {translate('sidebarScreen.tooltip')} - - ), - [ - styles.alignItemsCenter, - styles.flexRow, - styles.justifyContentCenter, - styles.flexWrap, - styles.textAlignCenter, - styles.gap1, - styles.quickActionTooltipSubtitle, - theme.tooltipHighlightText, - translate, - ], - ); - const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -180,17 +158,18 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts new file mode 100644 index 000000000000..dc2a761a4903 --- /dev/null +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -0,0 +1,118 @@ +import type {ValueOf} from 'type-fest'; +import {dismissProductTraining} from '@libs/actions/Welcome'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; + +const { + CONCEIRGE_LHN_GBR, + RENAME_SAVED_SEARCH, + WORKSAPCE_CHAT_CREATE, + QUICK_ACTION_BUTTON, + SEARCH_FILTER_BUTTON_TOOLTIP, + BOTTOM_NAV_INBOX_TOOLTIP, + LHN_WORKSPACE_CHAT_TOOLTIP, + GLOBAL_CREATE_TOOLTIP, +} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; + +type ProductTrainingTooltipName = ValueOf; + +type ShouldShowConditionProps = { + shouldUseNarrowLayout?: boolean; +}; + +type TooltipData = { + content: Array<{text: TranslationPaths; isBold: boolean}>; + onHideTooltip: () => void; + name: ProductTrainingTooltipName; + priority: number; + shouldShow: (props: ShouldShowConditionProps) => boolean; +}; + +const TOOLTIPS: Record = { + [CONCEIRGE_LHN_GBR]: { + content: [ + {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false}, + {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true}, + ], + onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR), + name: CONCEIRGE_LHN_GBR, + priority: 1300, + shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout, + }, + [RENAME_SAVED_SEARCH]: { + content: [ + {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH), + name: RENAME_SAVED_SEARCH, + priority: 1250, + shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout, + }, + [GLOBAL_CREATE_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.globalCreateTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.globalCreateTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.globalCreateTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(GLOBAL_CREATE_TOOLTIP), + name: GLOBAL_CREATE_TOOLTIP, + priority: 1200, + shouldShow: () => true, + }, + [QUICK_ACTION_BUTTON]: { + content: [ + {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true}, + {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON), + name: QUICK_ACTION_BUTTON, + priority: 1150, + shouldShow: () => true, + }, + [WORKSAPCE_CHAT_CREATE]: { + content: [ + {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: true}, + {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE), + name: WORKSAPCE_CHAT_CREATE, + priority: 1100, + shouldShow: () => true, + }, + [SEARCH_FILTER_BUTTON_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.searchFilterButtonTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.searchFilterButtonTooltip.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(SEARCH_FILTER_BUTTON_TOOLTIP), + name: SEARCH_FILTER_BUTTON_TOOLTIP, + priority: 1000, + shouldShow: () => true, + }, + [BOTTOM_NAV_INBOX_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP), + name: BOTTOM_NAV_INBOX_TOOLTIP, + priority: 900, + shouldShow: () => true, + }, + [LHN_WORKSPACE_CHAT_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.workspaceChatTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.workspaceChatTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.workspaceChatTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP), + name: LHN_WORKSPACE_CHAT_TOOLTIP, + priority: 800, + shouldShow: () => true, + }, +}; + +export default TOOLTIPS; +export type {ProductTrainingTooltipName}; diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx new file mode 100644 index 000000000000..7cfcf4d3bfa7 --- /dev/null +++ b/src/components/ProductTrainingContext/index.tsx @@ -0,0 +1,224 @@ +import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import type {ProductTrainingTooltipName} from './TOOLTIPS'; +import TOOLTIPS from './TOOLTIPS'; + +type ProductTrainingContextType = { + shouldRenderTooltip: (tooltipName: ProductTrainingTooltipName) => boolean; + registerTooltip: (tooltipName: ProductTrainingTooltipName) => void; + unregisterTooltip: (tooltipName: ProductTrainingTooltipName) => void; +}; + +const ProductTrainingContext = createContext({ + shouldRenderTooltip: () => false, + registerTooltip: () => {}, + unregisterTooltip: () => {}, +}); + +function ProductTrainingContextProvider({children}: ChildrenProps) { + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); + const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; + const [isOnboardingCompleted = true, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasCompletedGuidedSetupFlowSelector, + }); + const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const [activeTooltips, setActiveTooltips] = useState>(new Set()); + + const unregisterTooltip = useCallback( + (tooltipName: ProductTrainingTooltipName) => { + setActiveTooltips((prev) => { + const next = new Set(prev); + next.delete(tooltipName); + return next; + }); + }, + [setActiveTooltips], + ); + + const determineVisibleTooltip = useCallback(() => { + if (activeTooltips.size === 0) { + return null; + } + + const sortedTooltips = Array.from(activeTooltips) + .map((name) => ({ + name, + priority: TOOLTIPS[name]?.priority ?? 0, + })) + .sort((a, b) => b.priority - a.priority); + + const highestPriorityTooltip = sortedTooltips.at(0); + + if (!highestPriorityTooltip) { + return null; + } + + return highestPriorityTooltip.name; + }, [activeTooltips]); + + const shouldTooltipBeVisible = useCallback( + (tooltipName: ProductTrainingTooltipName) => { + if (isLoadingOnyxValue(isOnboardingCompletedMetadata)) { + return false; + } + + const isDismissed = !!dismissedProductTraining?.[tooltipName]; + + if (isDismissed) { + return false; + } + const tooltipConfig = TOOLTIPS[tooltipName]; + + // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip + if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) { + return false; + } + if (isOnboardingCompleted === false) { + return false; + } + + return tooltipConfig.shouldShow({ + shouldUseNarrowLayout, + }); + }, + [dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, isOnboardingCompletedMetadata, shouldUseNarrowLayout], + ); + + const registerTooltip = useCallback( + (tooltipName: ProductTrainingTooltipName) => { + const shouldRegister = shouldTooltipBeVisible(tooltipName); + if (!shouldRegister) { + return; + } + setActiveTooltips((prev) => new Set([...prev, tooltipName])); + }, + [shouldTooltipBeVisible], + ); + + const shouldRenderTooltip = useCallback( + (tooltipName: ProductTrainingTooltipName) => { + // First check base conditions + const shouldShow = shouldTooltipBeVisible(tooltipName); + if (!shouldShow) { + return false; + } + const visibleTooltip = determineVisibleTooltip(); + + // If this is the highest priority visible tooltip, show it + if (tooltipName === visibleTooltip) { + return true; + } + + return false; + }, + [shouldTooltipBeVisible, determineVisibleTooltip], + ); + + const contextValue = useMemo( + () => ({ + shouldRenderTooltip, + registerTooltip, + unregisterTooltip, + }), + [shouldRenderTooltip, registerTooltip, unregisterTooltip], + ); + + return {children}; +} + +const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shouldShow = true) => { + const context = useContext(ProductTrainingContext); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + if (!context) { + throw new Error('useProductTourContext must be used within a ProductTourProvider'); + } + + const {shouldRenderTooltip, registerTooltip, unregisterTooltip} = context; + + useEffect(() => { + if (shouldShow) { + registerTooltip(tooltipName); + return () => { + unregisterTooltip(tooltipName); + }; + } + return () => {}; + }, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]); + + const renderProductTrainingTooltip = useCallback(() => { + const tooltip = TOOLTIPS[tooltipName]; + return ( + + + + {tooltip.content.map(({text, isBold}) => { + const translatedText = translate(text); + return ( + + {translatedText} + + ); + })} + + + ); + }, [ + styles.alignItemsCenter, + styles.flexRow, + styles.flexWrap, + styles.gap3, + styles.justifyContentCenter, + styles.mw100, + styles.p2, + styles.productTrainingTooltipText, + styles.textAlignCenter, + styles.textBold, + styles.textWrap, + theme.tooltipHighlightText, + tooltipName, + translate, + ]); + + const shouldShowProductTrainingTooltip = useMemo(() => { + return shouldRenderTooltip(tooltipName); + }, [shouldRenderTooltip, tooltipName]); + + const hideProductTrainingTooltip = useCallback(() => { + const tooltip = TOOLTIPS[tooltipName]; + tooltip.onHideTooltip(); + unregisterTooltip(tooltipName); + }, [tooltipName, unregisterTooltip]); + + return { + renderProductTrainingTooltip, + hideProductTrainingTooltip, + shouldShowProductTrainingTooltip, + }; +}; + +export {ProductTrainingContextProvider, useProductTrainingContext}; diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index a78845f126d2..21a5832052c0 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import React, {useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -8,6 +9,8 @@ import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useProductTrainingContext} from '@components/ProductTrainingContext'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -55,6 +58,11 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); + const isFocused = useIsFocused(); + const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP, + isFocused, + ); const {status, hash} = queryJSON; @@ -348,12 +356,25 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { shouldUseStyleUtilityForAnchorPosition /> ) : ( -