diff --git a/src/CONST.ts b/src/CONST.ts
index 38af2f2d4f8a..672219eb1a7c 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -6992,6 +6992,9 @@ const CONST = {
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
SCAN_TEST_TOOLTIP_MANAGER: 'scanTestTooltipManager',
SCAN_TEST_CONFIRMATION: 'scanTestConfirmation',
+ OUTSANDING_FILTER: 'outstandingFilter',
+ SETTINGS_TAB: 'settingsTab',
+ WORKSPACES_SETTINGS: 'workspacesSettings',
GBR_RBR_CHAT: 'chatGBRRBR',
ACCOUNT_SWITCHER: 'accountSwitcher',
EXPENSE_REPORTS_FILTER: 'expenseReportsFilter',
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 61aac7d171ba..b5948b56fe79 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -350,6 +350,9 @@ type MenuItemBaseProps = {
/** Callback to fire when the education tooltip is pressed */
onEducationTooltipPress?: () => void;
+ /** Whether the tooltip should hide on scroll */
+ shouldHideOnScroll?: boolean;
+
shouldShowLoadingSpinnerIcon?: boolean;
/** Should selected item be marked with checkmark */
@@ -474,6 +477,7 @@ function MenuItem(
onBlur,
avatarID,
shouldRenderTooltip = false,
+ shouldHideOnScroll = false,
tooltipAnchorAlignment,
tooltipWrapperStyle = {},
tooltipShiftHorizontal = 0,
@@ -639,6 +643,7 @@ function MenuItem(
shiftVertical={tooltipShiftVertical}
shouldTeleportPortalToModalLayer={shouldTeleportPortalToModalLayer}
onTooltipPress={onEducationTooltipPress}
+ shouldHideOnScroll={shouldHideOnScroll}
>
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index 84b1e644a5ff..91c4f8ea8c06 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -62,10 +62,11 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
const [chatTabBrickRoad, setChatTabBrickRoad] = useState(undefined);
const platform = getPlatform();
const isWebOrDesktop = platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP;
- const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
- CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.BOTTOM_NAV_INBOX_TOOLTIP,
- isTooltipAllowed && selectedTab !== NAVIGATION_TABS.HOME,
- );
+ const {
+ renderProductTrainingTooltip: renderInboxTooltip,
+ shouldShowProductTrainingTooltip: shouldShowInboxTooltip,
+ hideProductTrainingTooltip: hideInboxTooltip,
+ } = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.BOTTOM_NAV_INBOX_TOOLTIP, isTooltipAllowed && selectedTab !== NAVIGATION_TABS.HOME);
const StyleUtils = useStyleUtils();
// On a wide layout DebugTabView should be rendered only within the navigation tab bar displayed directly on screens.
@@ -82,9 +83,9 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
return;
}
- hideProductTrainingTooltip();
+ hideInboxTooltip();
Navigation.navigate(ROUTES.HOME);
- }, [hideProductTrainingTooltip, selectedTab]);
+ }, [hideInboxTooltip, selectedTab]);
const navigateToSearch = useCallback(() => {
if (selectedTab === NAVIGATION_TABS.SEARCH) {
@@ -220,13 +221,13 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
/>
@@ -314,13 +317,13 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar
)}
diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts
index 513888c957e9..29ac4ce6f4a3 100644
--- a/src/components/ProductTrainingContext/TOOLTIPS.ts
+++ b/src/components/ProductTrainingContext/TOOLTIPS.ts
@@ -12,6 +12,9 @@ const {
SCAN_TEST_TOOLTIP,
SCAN_TEST_TOOLTIP_MANAGER,
SCAN_TEST_CONFIRMATION,
+ OUTSANDING_FILTER,
+ SETTINGS_TAB,
+ WORKSPACES_SETTINGS,
GBR_RBR_CHAT,
ACCOUNT_SWITCHER,
EXPENSE_REPORTS_FILTER,
@@ -160,6 +163,36 @@ const TOOLTIPS: Record = {
priority: 1100,
shouldShow: () => true,
},
+ [OUTSANDING_FILTER]: {
+ content: [
+ {text: 'productTrainingTooltip.outstandingFilter.part1', isBold: false},
+ {text: 'productTrainingTooltip.outstandingFilter.part2', isBold: true},
+ ],
+ onHideTooltip: () => dismissProductTraining(OUTSANDING_FILTER),
+ name: OUTSANDING_FILTER,
+ priority: 1925,
+ shouldShow: ({isUserPolicyAdmin}) => isUserPolicyAdmin,
+ },
+ [SETTINGS_TAB]: {
+ content: [
+ {text: 'productTrainingTooltip.settingsTab.part1', isBold: false},
+ {text: 'productTrainingTooltip.settingsTab.part2', isBold: true},
+ ],
+ onHideTooltip: () => dismissProductTraining(SETTINGS_TAB),
+ name: SETTINGS_TAB,
+ priority: 1750,
+ shouldShow: ({isUserPolicyAdmin}) => isUserPolicyAdmin,
+ },
+ [WORKSPACES_SETTINGS]: {
+ content: [
+ {text: 'productTrainingTooltip.workspacesSettings.part1', isBold: false},
+ {text: 'productTrainingTooltip.workspacesSettings.part2', isBold: true},
+ ],
+ onHideTooltip: () => dismissProductTraining(WORKSPACES_SETTINGS),
+ name: WORKSPACES_SETTINGS,
+ priority: 1550,
+ shouldShow: ({isUserPolicyAdmin}) => isUserPolicyAdmin,
+ },
};
export default TOOLTIPS;
diff --git a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx
index c5334dac2b35..60440901c7da 100644
--- a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx
+++ b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx
@@ -1,4 +1,5 @@
-import React, {useMemo, useRef} from 'react';
+import {useFocusEffect} from '@react-navigation/native';
+import React, {useCallback, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView as RNScrollView, ViewStyle} from 'react-native';
@@ -7,13 +8,16 @@ import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import * as Expensicons from '@components/Icon/Expensicons';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import ScrollView from '@components/ScrollView';
import {useSearchContext} from '@components/Search/SearchContext';
import type {ChatSearchStatus, ExpenseSearchStatus, InvoiceSearchStatus, SearchGroupBy, SearchQueryJSON, TripSearchStatus} from '@components/Search/types';
import SearchStatusSkeleton from '@components/Skeletons/SearchStatusSkeleton';
+import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useScrollEventEmitter from '@hooks/useScrollEventEmitter';
import useSingleExecution from '@hooks/useSingleExecution';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
@@ -229,6 +233,27 @@ function SearchStatusBar({queryJSON, onStatusChange, headerButtonsOptions}: Sear
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, {canBeMissing: false});
const {isOffline} = useNetwork();
+ const [isScreenFocused, setIsScreenFocused] = useState(false);
+
+ useFocusEffect(
+ useCallback(() => {
+ setIsScreenFocused(true);
+ return () => {
+ setIsScreenFocused(false);
+ };
+ }, []),
+ );
+ const isOutstandingStatusActive = Array.isArray(queryJSON.status)
+ ? queryJSON.status.includes(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING)
+ : queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING;
+ const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.OUTSANDING_FILTER,
+ isScreenFocused && !isOutstandingStatusActive,
+ );
+ // Controls the visibility of the educational tooltip based on user scrolling.
+ // Hides the tooltip when the user is scrolling and displays it once scrolling stops.
+ const triggerScrollEvent = useScrollEventEmitter();
+
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions ?? {}), [selectedTransactions]);
const shouldShowSelectedDropdown = headerButtonsOptions.length > 0 && (!shouldUseNarrowLayout || (!!selectionMode && selectionMode.isEnabled));
@@ -279,9 +304,16 @@ function SearchStatusBar({queryJSON, onStatusChange, headerButtonsOptions}: Sear
ref={scrollRef}
horizontal
showsHorizontalScrollIndicator={false}
+ onScroll={() => {
+ triggerScrollEvent();
+ }}
>
{options.map((item, index) => {
+ const isOutstanding = item.status === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING;
const onPress = singleExecution(() => {
+ if (isOutstanding) {
+ hideProductTrainingTooltip();
+ }
onStatusChange?.();
const query = buildSearchQueryString({...queryJSON, status: item.status});
Navigation.setParams({q: query});
@@ -291,27 +323,41 @@ function SearchStatusBar({queryJSON, onStatusChange, headerButtonsOptions}: Sear
const isLastItem = index === options.length - 1;
return (
-
);
},
- [styles.pb4, styles.mh3, styles.sectionTitle, styles.sectionMenuItem, translate, userWallet?.currentBalance, focusedRouteName, isExecuting, singleExecution],
+ [
+ styles.pb4,
+ styles.mh3,
+ styles.sectionTitle,
+ styles.sectionMenuItem,
+ translate,
+ userWallet?.currentBalance,
+ focusedRouteName,
+ isExecuting,
+ singleExecution,
+ styles.productTrainingTooltipWrapper,
+ ],
);
const accountMenuItems = useMemo(() => getMenuItemsSection(accountMenuItemsData), [accountMenuItemsData, getMenuItemsSection]);
@@ -426,8 +484,9 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
return;
}
saveScrollOffset(route, e.nativeEvent.contentOffset.y);
+ triggerScrollEvent();
},
- [route, saveScrollOffset],
+ [route, saveScrollOffset, triggerScrollEvent],
);
useLayoutEffect(() => {
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 00ad83ed5b05..8b4c99918906 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -277,9 +277,12 @@ export default {
savedSearchShiftHorizontal: -10,
savedSearchShiftVertical: 6,
navigationTabBarInboxTooltipShiftHorizontal: 36,
+ navigationTabBarSettingsTooltipShiftHorizontal: -36,
accountSwitcherTooltipShiftVertical: 7,
accountSwitcherTooltipShiftHorizontal: 4,
expenseReportsTypeTooltipShiftHorizontal: 10,
+ workspacesSettingsTooltipShiftHorizontal: 8,
+ workspacesSettingsTooltipShiftVertical: -8,
inlineImagePreviewMinSize: 64,
inlineImagePreviewMaxSize: 148,
diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts
index 7bea49cc963e..51832f0ae2e0 100644
--- a/src/types/onyx/DismissedProductTraining.ts
+++ b/src/types/onyx/DismissedProductTraining.ts
@@ -9,6 +9,9 @@ const {
SCAN_TEST_TOOLTIP,
SCAN_TEST_TOOLTIP_MANAGER,
SCAN_TEST_CONFIRMATION,
+ OUTSANDING_FILTER,
+ SETTINGS_TAB,
+ WORKSPACES_SETTINGS,
ACCOUNT_SWITCHER,
GBR_RBR_CHAT,
EXPENSE_REPORTS_FILTER,
@@ -75,6 +78,21 @@ type DismissedProductTraining = {
*/
[SCAN_TEST_CONFIRMATION]: DismissedProductTrainingElement;
+ /**
+ * When user dismisses the outstanding filter product training tooltip, we store the timestamp here.
+ */
+ [OUTSANDING_FILTER]: DismissedProductTrainingElement;
+
+ /**
+ * When user dismisses the settings tab product training tooltip, we store the timestamp here.
+ */
+ [SETTINGS_TAB]: DismissedProductTrainingElement;
+
+ /**
+ * When user dismisses the workspaces settings product training tooltip, we store the timestamp here.
+ */
+ [WORKSPACES_SETTINGS]: DismissedProductTrainingElement;
+
/**
* When user dismisses the accountSwitcher product training tooltip, we store the timestamp here.
*/
diff --git a/tests/navigation/GoBackTests.tsx b/tests/navigation/GoBackTests.tsx
index 0c7af7da4459..c28aefd44817 100644
--- a/tests/navigation/GoBackTests.tsx
+++ b/tests/navigation/GoBackTests.tsx
@@ -13,6 +13,14 @@ import TestNavigationContainer from '../utils/TestNavigationContainer';
jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
jest.mock('@libs/getIsNarrowLayout', () => jest.fn());
+// Mock Fullstory library dependency
+jest.mock('@libs/Fullstory', () => ({
+ default: {
+ consentAndIdentify: jest.fn(),
+ },
+ parseFSAttributes: jest.fn(),
+}));
+
jest.mock('@pages/home/sidebar/NavigationTabBarAvatar');
jest.mock('@src/components/Navigation/TopLevelNavigationTabBar');
jest.mock('@components/ConfirmedRoute.tsx');
diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx
index 608caf734392..ef1ebe882845 100644
--- a/tests/navigation/NavigateTests.tsx
+++ b/tests/navigation/NavigateTests.tsx
@@ -13,6 +13,14 @@ import TestNavigationContainer from '../utils/TestNavigationContainer';
jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
jest.mock('@libs/getIsNarrowLayout', () => jest.fn());
+// Mock Fullstory library dependency
+jest.mock('@libs/Fullstory', () => ({
+ default: {
+ consentAndIdentify: jest.fn(),
+ },
+ parseFSAttributes: jest.fn(),
+}));
+
jest.mock('@pages/home/sidebar/NavigationTabBarAvatar');
jest.mock('@src/components/Navigation/TopLevelNavigationTabBar');
jest.mock('@components/ConfirmedRoute.tsx');
diff --git a/tests/navigation/SwitchPolicyIDTests.tsx b/tests/navigation/SwitchPolicyIDTests.tsx
index 418c974573f5..1c62132ec94b 100644
--- a/tests/navigation/SwitchPolicyIDTests.tsx
+++ b/tests/navigation/SwitchPolicyIDTests.tsx
@@ -12,6 +12,14 @@ import TestNavigationContainer from '../utils/TestNavigationContainer';
jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
jest.mock('@libs/getIsNarrowLayout', () => jest.fn());
+// Mock Fullstory library dependency
+jest.mock('@libs/Fullstory', () => ({
+ default: {
+ consentAndIdentify: jest.fn(),
+ },
+ parseFSAttributes: jest.fn(),
+}));
+
jest.mock('@pages/home/sidebar/NavigationTabBarAvatar');
jest.mock('@src/components/Navigation/TopLevelNavigationTabBar');
jest.mock('@components/ConfirmedRoute.tsx');