diff --git a/src/CONST.ts b/src/CONST.ts
index 1538ae6b3c16..5205712e4535 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -6656,6 +6656,8 @@ const CONST = {
LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip',
GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip',
SCAN_TEST_TOOLTIP: 'scanTestTooltip',
+ SCAN_TEST_TOOLTIP_MANAGER: 'scanTestTooltipManager',
+ SCAN_TEST_CONFIRMATION: 'scanTestConfirmation',
},
SMART_BANNER_HEIGHT: 152,
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 050f2a6e403b..37ab583a11e3 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -31,7 +31,8 @@ import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransaction
import Log from '@libs/Log';
import {validateAmount} from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getIOUConfirmationOptionsFromPayeePersonalDetail, hasEnabledOptions} from '@libs/OptionsListUtils';
+import {getIOUConfirmationOptionsFromPayeePersonalDetail, hasEnabledOptions, isSelectedManagerMcTest} from '@libs/OptionsListUtils';
+import Permissions from '@libs/Permissions';
import {getDistanceRateCustomUnitRate, getTagLists, isTaxTrackingEnabled} from '@libs/PolicyUtils';
import type {OptionData} from '@libs/ReportUtils';
import playSound, {SOUNDS} from '@libs/Sound';
@@ -65,11 +66,13 @@ import FormHelpMessage from './FormHelpMessage';
import MoneyRequestAmountInput from './MoneyRequestAmountInput';
import MoneyRequestConfirmationListFooter from './MoneyRequestConfirmationListFooter';
import {PressableWithFeedback} from './Pressable';
+import {useProductTrainingContext} from './ProductTrainingContext';
import SelectionList from './SelectionList';
import type {SectionListDataType} from './SelectionList/types';
import UserListItem from './SelectionList/UserListItem';
import SettlementButton from './SettlementButton';
import Text from './Text';
+import EducationalTooltip from './Tooltip/EducationalTooltip';
type MoneyRequestConfirmationListProps = {
/** Callback to inform parent modal of success */
@@ -215,6 +218,11 @@ function MoneyRequestConfirmationList({
const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${policyID}`);
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_CONFIRMATION,
+ Permissions.canUseManagerMcTest(betas) && selectedParticipantsProp.some((participant) => isSelectedManagerMcTest(participant.login)),
+ );
const policy = policyReal ?? policyDraft;
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
@@ -966,11 +974,37 @@ function MoneyRequestConfirmationList({
message={errorMessage}
/>
)}
-
- {button}
+
+ {button}
+
>
);
- }, [isReadOnly, iouType, confirm, isConfirmed, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, styles.ph1, styles.mb2, errorMessage]);
+ }, [
+ isReadOnly,
+ iouType,
+ confirm,
+ bankAccountRoute,
+ iouCurrencyCode,
+ policyID,
+ isConfirmed,
+ splitOrRequestOptions,
+ errorMessage,
+ styles.ph1,
+ styles.mb2,
+ styles.productTrainingTooltipWrapper,
+ shouldShowProductTrainingTooltip,
+ renderProductTrainingTooltip,
+ ]);
const listFooterContent = (
;
@@ -119,18 +121,34 @@ const TOOLTIPS: Record = {
content: [
{text: 'productTrainingTooltip.scanTestTooltip.part1', isBold: false},
{text: 'productTrainingTooltip.scanTestTooltip.part2', isBold: true},
+ ],
+ onHideTooltip: () => dismissProductTraining(SCAN_TEST_TOOLTIP),
+ name: SCAN_TEST_TOOLTIP,
+ priority: 900,
+ shouldShow: () => true,
+ shouldRenderActionButtons: true,
+ },
+ [SCAN_TEST_TOOLTIP_MANAGER]: {
+ content: [
{text: 'productTrainingTooltip.scanTestTooltip.part3', isBold: false},
{text: 'productTrainingTooltip.scanTestTooltip.part4', isBold: true},
{text: 'productTrainingTooltip.scanTestTooltip.part5', isBold: false},
+ ],
+ onHideTooltip: () => dismissProductTraining(SCAN_TEST_TOOLTIP_MANAGER),
+ name: SCAN_TEST_TOOLTIP_MANAGER,
+ priority: 1000,
+ shouldShow: () => true,
+ },
+ [SCAN_TEST_CONFIRMATION]: {
+ content: [
{text: 'productTrainingTooltip.scanTestTooltip.part6', isBold: false},
{text: 'productTrainingTooltip.scanTestTooltip.part7', isBold: true},
{text: 'productTrainingTooltip.scanTestTooltip.part8', isBold: false},
],
- onHideTooltip: () => dismissProductTraining(SCAN_TEST_TOOLTIP),
- name: SCAN_TEST_TOOLTIP,
- priority: 900,
- shouldShow: () => false,
- shouldRenderActionButtons: true,
+ onHideTooltip: () => dismissProductTraining(SCAN_TEST_CONFIRMATION),
+ name: SCAN_TEST_CONFIRMATION,
+ priority: 1100,
+ shouldShow: () => true,
},
};
diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx
index 820552901452..8bb3c20572c9 100644
--- a/src/components/ProductTrainingContext/index.tsx
+++ b/src/components/ProductTrainingContext/index.tsx
@@ -101,7 +101,13 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
}
// We need to make an exception for the QAB tooltip because it is shown in a modal, otherwise it would be hidden if a modal is visible
- if (tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.QUICK_ACTION_BUTTON && isModalVisible) {
+ if (
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.QUICK_ACTION_BUTTON &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP_MANAGER &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_CONFIRMATION &&
+ isModalVisible
+ ) {
return false;
}
@@ -131,7 +137,6 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
return false;
}
const visibleTooltip = determineVisibleTooltip();
-
// If this is the highest priority visible tooltip, show it
if (tooltipName === visibleTooltip) {
return true;
@@ -249,7 +254,6 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
const shouldShowProductTrainingTooltip = useMemo(() => {
return shouldShow && shouldRenderTooltip(tooltipName);
}, [shouldRenderTooltip, tooltipName, shouldShow]);
-
const hideProductTrainingTooltip = useCallback(() => {
if (!shouldShowProductTrainingTooltip) {
return;
diff --git a/src/components/SelectionList/InviteMemberListItem.tsx b/src/components/SelectionList/InviteMemberListItem.tsx
index 7442fe24f7d2..d399ff8ade65 100644
--- a/src/components/SelectionList/InviteMemberListItem.tsx
+++ b/src/components/SelectionList/InviteMemberListItem.tsx
@@ -1,18 +1,24 @@
import {Str} from 'expensify-common';
import React, {useCallback} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import SelectCircle from '@components/SelectCircle';
import SubscriptAvatar from '@components/SubscriptAvatar';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
+import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getIsUserSubmittedExpenseOrScannedReceipt, isSelectedManagerMcTest} from '@libs/OptionsListUtils';
+import Permissions from '@libs/Permissions';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {Icon} from '@src/types/onyx/OnyxCommon';
import BaseListItem from './BaseListItem';
import type {InviteMemberListItemProps, ListItem} from './types';
@@ -42,6 +48,11 @@ function InviteMemberListItem({
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip} = useProductTrainingContext(
+ CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP_MANAGER,
+ !getIsUserSubmittedExpenseOrScannedReceipt() && Permissions.canUseManagerMcTest(betas) && isSelectedManagerMcTest(item.login),
+ );
const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
@@ -84,64 +95,75 @@ function InviteMemberListItem({
shouldDisplayRBR={!shouldShowCheckBox}
>
{(hovered?: boolean) => (
- <>
- {!!item.icons &&
- (item.shouldShowSubscript ? (
-
- ) : (
-
- ))}
-
-
-
+
+
+ {!!item.icons &&
+ (item.shouldShowSubscript ? (
+
+ ) : (
+
+ ))}
+
+
+
+
+ {!!item.alternateText && (
+
+ )}
- {!!item.alternateText && (
-
+ {!!item.rightElement && item.rightElement}
+ {!!shouldShowCheckBox && (
+
+
+
)}
- {!!item.rightElement && item.rightElement}
- {!!shouldShowCheckBox && (
-
-
-
- )}
- >
+
)}
);
diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx
index cc35dc482742..1813ef21f0a8 100644
--- a/src/components/TabSelector/TabSelector.tsx
+++ b/src/components/TabSelector/TabSelector.tsx
@@ -1,13 +1,17 @@
import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs/lib/typescript/src/types';
import React, {useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement';
import * as Expensicons from '@components/Icon/Expensicons';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import {getIsUserSubmittedExpenseOrScannedReceipt} from '@libs/OptionsListUtils';
+import Permissions from '@libs/Permissions';
import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
import type IconAsset from '@src/types/utils/IconAsset';
import getBackgroundColor from './getBackground';
import getOpacity from './getOpacity';
@@ -54,6 +58,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
const styles = useThemeStyles();
const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]);
const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs);
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
useEffect(() => {
// It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition.
@@ -71,7 +76,8 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
const inactiveOpacity = getOpacity({routesLength: state.routes.length, tabIndex: index, active: false, affectedTabs: affectedAnimatedTabs, position, isActive});
const backgroundColor = getBackgroundColor({routesLength: state.routes.length, tabIndex: index, affectedTabs: affectedAnimatedTabs, theme, position, isActive});
const {icon, title} = getIconAndTitle(route.name, translate);
-
+ const shouldShowTestReceiptTooltip =
+ route.name === CONST.TAB_REQUEST.SCAN && isActive && !getIsUserSubmittedExpenseOrScannedReceipt() && Permissions.canUseManagerMcTest(betas);
const onPress = () => {
if (isActive) {
return;
@@ -104,6 +110,7 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
backgroundColor={backgroundColor}
isActive={isActive}
shouldShowLabelWhenInactive={shouldShowLabelWhenInactive}
+ shouldShowTestReceiptTooltip={shouldShowTestReceiptTooltip}
/>
);
})}
@@ -113,7 +120,6 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu
}
TabSelector.displayName = 'TabSelector';
-
export default TabSelector;
export type {TabSelectorProps};
diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx
index ea5ffc00d81d..b7201a35553e 100644
--- a/src/components/TabSelector/TabSelectorItem.tsx
+++ b/src/components/TabSelector/TabSelectorItem.tsx
@@ -2,7 +2,9 @@ import React, {useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import {Animated} from 'react-native';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import {useProductTrainingContext} from '@components/ProductTrainingContext';
import Tooltip from '@components/Tooltip';
+import EducationalTooltip from '@components/Tooltip/EducationalTooltip';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
@@ -35,6 +37,9 @@ type TabSelectorItemProps = {
/** Whether to show the label when the tab is inactive */
shouldShowLabelWhenInactive?: boolean;
+
+ /** Whether to show the test receipt tooltip */
+ shouldShowTestReceiptTooltip?: boolean;
};
function TabSelectorItem({
@@ -46,15 +51,14 @@ function TabSelectorItem({
inactiveOpacity = 1,
isActive = false,
shouldShowLabelWhenInactive = true,
+ shouldShowTestReceiptTooltip = false,
}: TabSelectorItemProps) {
const styles = useThemeStyles();
const [isHovered, setIsHovered] = useState(false);
+ const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, shouldShowTestReceiptTooltip);
- return (
-
+ const content = () => {
+ return (
)}
+ );
+ };
+
+ return shouldShowTestReceiptTooltip ? (
+
+ {content()}
+
+ ) : (
+
+ {content()}
);
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 361251ea1a96..163e5355b8bb 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -5938,7 +5938,7 @@ const translations = {
part5: ' to try it out!',
part6: 'Now,',
part7: ' submit your expense',
- part8: ' and watch the magic happen!',
+ part8: ' and watch the\nmagic happen!',
tryItOut: 'Try it out',
noThanks: 'No thanks',
},
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 66b70d90c171..4067bfadaf0f 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -1482,7 +1482,7 @@ function getValidOptions(
[CONST.EMAIL.NOTIFICATIONS]: true,
...excludeLogins,
// Exclude Manager McTest if user submitted expense or scanned receipt and when selection is made from Create or Submit flow
- [CONST.EMAIL.MANAGER_MCTEST]: !(Permissions.canUseManagerMcTest(config.betas) && !getIsUserSubmittedExpenseOrScannedReceipt() && canShowManagerMcTest),
+ [CONST.EMAIL.MANAGER_MCTEST]: !(!getIsUserSubmittedExpenseOrScannedReceipt() && canShowManagerMcTest && Permissions.canUseManagerMcTest(config.betas)),
};
// If we're including selected options from the search results, we only want to exclude them if the search input is empty
// This is because on certain pages, we show the selected options at the top when the search input is empty
@@ -2145,6 +2145,13 @@ function shouldUseBoldText(report: OptionData): boolean {
return report.isUnread === true && notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE && !isHiddenForCurrentUser(notificationPreference);
}
+/**
+ * Helper method to check if participant email is Manager McTest
+ */
+function isSelectedManagerMcTest(email: string | null | undefined): boolean {
+ return email === CONST.EMAIL.MANAGER_MCTEST;
+}
+
export {
getAvatarsForAccountIDs,
isCurrentUser,
@@ -2198,6 +2205,8 @@ export {
orderWorkspaceOptions,
filterSelfDMChat,
filterReports,
+ getIsUserSubmittedExpenseOrScannedReceipt,
+ isSelectedManagerMcTest,
};
export type {Section, SectionBase, MemberForList, Options, OptionList, SearchOption, PayeePersonalDetails, Option, OptionTree, ReportAndPersonalDetailOptions, GetUserToInviteConfig};
diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts
index 0ab9425398bd..ae6079b4b632 100644
--- a/src/types/onyx/DismissedProductTraining.ts
+++ b/src/types/onyx/DismissedProductTraining.ts
@@ -10,6 +10,8 @@ const {
LHN_WORKSPACE_CHAT_TOOLTIP,
GLOBAL_CREATE_TOOLTIP,
SCAN_TEST_TOOLTIP,
+ SCAN_TEST_TOOLTIP_MANAGER,
+ SCAN_TEST_CONFIRMATION,
} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;
/**
* This type is used to store the timestamp of when the user dismisses a product training ui elements.
@@ -64,6 +66,16 @@ type DismissedProductTraining = {
* When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
*/
[SCAN_TEST_TOOLTIP]: string;
+
+ /**
+ * When user dismisses the test manager tooltip product training tooltip, we store the timestamp here.
+ */
+ [SCAN_TEST_TOOLTIP_MANAGER]: string;
+
+ /**
+ * When user dismisses the test manager on confirmantion page product training tooltip, we store the timestamp here.
+ */
+ [SCAN_TEST_CONFIRMATION]: string;
};
export default DismissedProductTraining;
diff --git a/tests/ui/components/TabSelectorItem.tsx b/tests/ui/components/TabSelectorItem.tsx
index 5fb0274a38c4..0e595a610319 100644
--- a/tests/ui/components/TabSelectorItem.tsx
+++ b/tests/ui/components/TabSelectorItem.tsx
@@ -5,6 +5,12 @@ import Tooltip from '@components/Tooltip';
// Mock the Tooltip component since it uses portals which aren't supported in RNTL
jest.mock('@components/Tooltip');
+jest.mock('@libs/Fullstory', () => ({
+ default: {
+ consentAndIdentify: jest.fn(),
+ },
+ parseFSAttributes: jest.fn(),
+}));
describe('TabSelectorItem Component', () => {
const title = 'Test Tab';