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';