diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index f0867bd82beb..55309c797eac 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -22,6 +22,12 @@ type TabSelectorProps = MaterialTopTabBarProps & { /** Whether to show the label when the tab is inactive */ shouldShowLabelWhenInactive?: boolean; + + /** Determines whether the product training tooltip should be displayed to the user. */ + shouldShowProductTrainingTooltip?: boolean; + + /** Function to render the content of the product training tooltip. */ + renderProductTrainingTooltip?: () => React.JSX.Element; }; type IconTitleAndTestID = { @@ -53,7 +59,16 @@ function getIconTitleAndTestID(route: string, translate: LocaleContextProps['tra } } -function TabSelector({state, navigation, onTabPress = () => {}, position, onFocusTrapContainerElementChanged, shouldShowLabelWhenInactive = true}: TabSelectorProps) { +function TabSelector({ + state, + navigation, + onTabPress = () => {}, + position, + onFocusTrapContainerElementChanged, + shouldShowLabelWhenInactive = true, + shouldShowProductTrainingTooltip = false, + renderProductTrainingTooltip, +}: TabSelectorProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -109,6 +124,8 @@ function TabSelector({state, navigation, onTabPress = () => {}, position, onFocu isActive={isActive} testID={testID} shouldShowLabelWhenInactive={shouldShowLabelWhenInactive} + shouldShowProductTrainingTooltip={shouldShowProductTrainingTooltip} + renderProductTrainingTooltip={renderProductTrainingTooltip} /> ); })} diff --git a/src/components/TabSelector/TabSelectorItem.tsx b/src/components/TabSelector/TabSelectorItem.tsx index 61e1f3d3e748..3c26f55b2952 100644 --- a/src/components/TabSelector/TabSelectorItem.tsx +++ b/src/components/TabSelector/TabSelectorItem.tsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; 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'; @@ -38,6 +39,12 @@ type TabSelectorItemProps = { /** Test identifier used to find elements in unit and e2e tests */ testID?: string; + + /** Determines whether the product training tooltip should be displayed to the user. */ + shouldShowProductTrainingTooltip?: boolean; + + /** Function to render the content of the product training tooltip. */ + renderProductTrainingTooltip?: () => React.JSX.Element; }; function TabSelectorItem({ @@ -50,39 +57,61 @@ function TabSelectorItem({ isActive = false, shouldShowLabelWhenInactive = true, testID, + shouldShowProductTrainingTooltip = false, + renderProductTrainingTooltip, }: TabSelectorItemProps) { const styles = useThemeStyles(); const [isHovered, setIsHovered] = useState(false); - return ( - setIsHovered(true)} + onHoverOut={() => setIsHovered(false)} + role={CONST.ROLE.BUTTON} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + testID={testID} > - setIsHovered(true)} - onHoverOut={() => setIsHovered(false)} - role={CONST.ROLE.BUTTON} - dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} - testID={testID} - > - + {(shouldShowLabelWhenInactive || isActive) && ( + - {(shouldShowLabelWhenInactive || isActive) && ( - - )} - + )} + + ); + + return shouldShowEducationalTooltip ? ( + + {children} + + ) : ( + + {children} ); } diff --git a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx index ad37f59c8ef5..13681aae6344 100644 --- a/src/components/Tooltip/BaseGenericTooltip/index.native.tsx +++ b/src/components/Tooltip/BaseGenericTooltip/index.native.tsx @@ -41,6 +41,7 @@ function BaseGenericTooltip({ shouldTeleportPortalToModalLayer = false, isEducationTooltip = false, onTooltipPress = () => {}, + computeHorizontalShiftForNative = false, }: BaseGenericTooltipProps) { // The width of tooltip's inner content. Has to be undefined in the beginning // as a width of 0 will cause the content to be rendered of a width of 0, @@ -74,6 +75,7 @@ function BaseGenericTooltip({ wrapperStyle, shouldAddHorizontalPadding: false, isEducationTooltip, + computeHorizontalShiftForNative, }), [ StyleUtils, @@ -91,6 +93,7 @@ function BaseGenericTooltip({ anchorAlignment, wrapperStyle, isEducationTooltip, + computeHorizontalShiftForNative, ], ); diff --git a/src/components/Tooltip/BaseGenericTooltip/types.ts b/src/components/Tooltip/BaseGenericTooltip/types.ts index bfd38cf73190..0d95b1ced518 100644 --- a/src/components/Tooltip/BaseGenericTooltip/types.ts +++ b/src/components/Tooltip/BaseGenericTooltip/types.ts @@ -38,7 +38,16 @@ type BaseGenericTooltipProps = { isEducationTooltip?: boolean; } & Pick< SharedTooltipProps, - 'renderTooltipContent' | 'maxWidth' | 'numberOfLines' | 'text' | 'shouldForceRenderingBelow' | 'wrapperStyle' | 'anchorAlignment' | 'shouldUseOverlay' | 'onTooltipPress' + | 'renderTooltipContent' + | 'maxWidth' + | 'numberOfLines' + | 'text' + | 'shouldForceRenderingBelow' + | 'wrapperStyle' + | 'anchorAlignment' + | 'shouldUseOverlay' + | 'onTooltipPress' + | 'computeHorizontalShiftForNative' >; // eslint-disable-next-line import/prefer-default-export diff --git a/src/components/Tooltip/GenericTooltip.tsx b/src/components/Tooltip/GenericTooltip.tsx index 811360cc8228..091435d8693c 100644 --- a/src/components/Tooltip/GenericTooltip.tsx +++ b/src/components/Tooltip/GenericTooltip.tsx @@ -38,6 +38,7 @@ function GenericTooltip({ shouldRender = true, isEducationTooltip = false, onTooltipPress = () => {}, + computeHorizontalShiftForNative = false, }: GenericTooltipProps) { const {preferredLocale} = useLocalize(); const {windowWidth} = useWindowDimensions(); @@ -189,6 +190,7 @@ function GenericTooltip({ shouldTeleportPortalToModalLayer={shouldTeleportPortalToModalLayer} onHideTooltip={onPressOverlay} onTooltipPress={onTooltipPress} + computeHorizontalShiftForNative={computeHorizontalShiftForNative} /> )} {/* eslint-disable-next-line react-compiler/react-compiler */} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index c34e86b528b3..927248922a90 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -45,6 +45,9 @@ type SharedTooltipProps = { /** Callback when tooltip is clicked */ onTooltipPress?: (event: GestureResponderEvent | KeyboardEvent | undefined) => void; + + /** Whether to compute horizontal shift for native */ + computeHorizontalShiftForNative?: boolean; }; type GenericTooltipState = { diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index d75c15fa8cf9..7ce48e3ac7ea 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -50,6 +50,12 @@ type OnyxTabNavigatorProps = ChildrenProps & { /** Disable swipe between tabs */ disableSwipe?: boolean; + /** Determines whether the product training tooltip should be displayed to the user. */ + shouldShowProductTrainingTooltip?: boolean; + + /** Function to render the content of the product training tooltip. */ + renderProductTrainingTooltip?: () => React.JSX.Element; + /** Whether to lazy load the tab screens */ lazyLoadEnabled?: boolean; @@ -78,6 +84,8 @@ function OnyxTabNavigator({ screenListeners, shouldShowLabelWhenInactive = true, disableSwipe = false, + shouldShowProductTrainingTooltip, + renderProductTrainingTooltip, lazyLoadEnabled = false, onTabSelect, ...rest @@ -114,12 +122,14 @@ function OnyxTabNavigator({ ); }, - [TabBar, onTabBarFocusTrapContainerElementChanged, shouldShowLabelWhenInactive], + [TabBar, onTabBarFocusTrapContainerElementChanged, shouldShowLabelWhenInactive, shouldShowProductTrainingTooltip, renderProductTrainingTooltip], ); // If the selected tab changes, we need to update the focus trap container element of the active tab diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 8e39b68152a5..7e95f8af6022 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -1,10 +1,11 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {useProductTrainingContext} from '@components/ProductTrainingContext'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import useLocalize from '@hooks/useLocalize'; @@ -12,11 +13,13 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import {dismissProductTraining} from '@libs/actions/Welcome'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TabScreenWithFocusTrapWrapper, TopTab} from '@libs/Navigation/OnyxTabNavigator'; +import {getIsUserSubmittedExpenseOrScannedReceipt} from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; -import {getPerDiemCustomUnit, getPerDiemCustomUnits} from '@libs/PolicyUtils'; +import {getPerDiemCustomUnit, getPerDiemCustomUnits, isUserInvitedToWorkspace} from '@libs/PolicyUtils'; import {getPayeeName} from '@libs/ReportUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {IOURequestType} from '@userActions/IOU'; @@ -139,6 +142,19 @@ function IOURequestStartPage({ iouType !== CONST.IOU.TYPE.SPLIT && iouType !== CONST.IOU.TYPE.TRACK && ((!isFromGlobalCreate && doesCurrentPolicyPerDiemExist) || (isFromGlobalCreate && doesPerDiemPolicyExist)); const {isBetaEnabled} = usePermissions(); + const setTestReceiptAndNavigateRef = useRef<() => void>(); + const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, + !getIsUserSubmittedExpenseOrScannedReceipt() && isBetaEnabled(CONST.BETAS.NEWDOT_MANAGER_MCTEST) && selectedTab === CONST.TAB_REQUEST.SCAN && !isUserInvitedToWorkspace(), + { + onConfirm: () => { + setTestReceiptAndNavigateRef?.current?.(); + }, + onDismiss: () => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, true); + }, + }, + ); return ( @@ -199,10 +217,12 @@ function IOURequestStartPage({ { + setTestReceiptAndNavigateRef.current = setTestReceiptAndNavigate; + }} setTabSwipeDisabled={setSwipeDisabled} isMultiScanEnabled={isMultiScanEnabled} setIsMultiScanEnabled={setIsMultiScanEnabled} - isTooltipAllowed /> )} diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 8de6aaaf3728..62d37bdc7de5 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect, useIsFocused} from '@react-navigation/core'; +import {useFocusEffect} from '@react-navigation/core'; import {format} from 'date-fns'; import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useRef, useState} from 'react'; @@ -25,17 +25,13 @@ import ImageSVG from '@components/ImageSVG'; import LocationPermissionModal from '@components/LocationPermissionModal'; import PDFThumbnail from '@components/PDFThumbnail'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import {useProductTrainingContext} from '@components/ProductTrainingContext'; import Text from '@components/Text'; -import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import setTestReceipt from '@libs/actions/setTestReceipt'; -import {dismissProductTraining} from '@libs/actions/Welcome'; import {readFileAsync, resizeImageIfNeeded, showCameraPermissionsAlert, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; import getPhotoSource from '@libs/fileDownload/getPhotoSource'; import convertHeicImage from '@libs/fileDownload/heicConverter'; @@ -46,15 +42,14 @@ import HapticFeedback from '@libs/HapticFeedback'; import {navigateToParticipantPage, shouldStartLocationPermissionFlow} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import {getIsUserSubmittedExpenseOrScannedReceipt, getManagerMcTestParticipant, getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; -import {isPaidGroupPolicy, isUserInvitedToWorkspace} from '@libs/PolicyUtils'; +import {getManagerMcTestParticipant, getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {generateReportID, getPolicyExpenseChat, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import {getDefaultTaxCode} from '@libs/TransactionUtils'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; -import variables from '@styles/variables'; import { getMoneyRequestParticipantsFromReport, replaceReceipt, @@ -89,10 +84,10 @@ function IOURequestStepScan({ }, transaction: initialTransaction, currentUserPersonalDetails, + onLayout, setTabSwipeDisabled, isMultiScanEnabled = false, setIsMultiScanEnabled, - isTooltipAllowed = false, }: IOURequestStepScanProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -100,9 +95,7 @@ function IOURequestStepScan({ const device = useCameraDevice('back', { physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'], }); - const {isBetaEnabled} = usePermissions(); - const [elementTop, setElementTop] = useState(0); const isEditing = action === CONST.IOU.ACTION.EDIT; const hasFlash = !!device?.hasFlash; const camera = useRef(null); @@ -123,7 +116,6 @@ function IOURequestStepScan({ const isPlatformMuted = mutedPlatforms[platform]; const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); - const isTabActive = useIsFocused(); const [pdfFile, setPdfFile] = useState(null); @@ -532,17 +524,6 @@ function IOURequestStepScan({ }); }, [initialTransactionID, isEditing, navigateToConfirmationStep]); - const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, - isTooltipAllowed && !getIsUserSubmittedExpenseOrScannedReceipt() && isBetaEnabled(CONST.BETAS.NEWDOT_MANAGER_MCTEST) && isTabActive && !isUserInvitedToWorkspace(), - { - onConfirm: setTestReceiptAndNavigate, - onDismiss: () => { - dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, true); - }, - }, - ); - /** * Converts HEIC image to JPEG using promises */ @@ -770,8 +751,8 @@ function IOURequestStepScan({ > { - setElementTop(e.nativeEvent.layout.height - (variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding) * 2); + onLayout={() => { + onLayout(setTestReceiptAndNavigate); }} > {!!pdfFile && ( @@ -794,88 +775,76 @@ function IOURequestStepScan({ }} /> )} - - - {cameraPermissionStatus !== RESULTS.GRANTED && ( - - + + {cameraPermissionStatus !== RESULTS.GRANTED && ( + + - {translate('receipt.takePhoto')} - {translate('receipt.cameraAccess')} -