diff --git a/assets/images/educational-illustration__multi-scan.svg b/assets/images/educational-illustration__multi-scan.svg new file mode 100644 index 0000000000000..601be9c94d2ea --- /dev/null +++ b/assets/images/educational-illustration__multi-scan.svg @@ -0,0 +1,380 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 53c4c260e6102..fcb05ff99c92d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -7126,6 +7126,7 @@ const CONST = { ACCOUNT_SWITCHER: 'accountSwitcher', EXPENSE_REPORTS_FILTER: 'expenseReportsFilter', SCAN_TEST_DRIVE_CONFIRMATION: 'scanTestDriveConfirmation', + MULTI_SCAN_EDUCATIONAL_MODAL: 'multiScanEducationalModal', }, CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', SMART_BANNER_HEIGHT: 152, diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 41755fee377ef..372b8c80f2995 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -2,7 +2,7 @@ import type {VideoReadyForDisplayEvent} from 'expo-av'; import type {ImageContentFit} from 'expo-image'; import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react'; import {Image, InteractionManager, View} from 'react-native'; -import type {ImageResizeMode, ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; +import type {ImageResizeMode, ImageSourcePropType, StyleProp, TextStyle, ViewStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import type {MergeExclusive} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; @@ -20,6 +20,7 @@ import Button from './Button'; import CheckboxWithLabel from './CheckboxWithLabel'; import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; import ImageSVG from './ImageSVG'; +import type ImageSVGProps from './ImageSVG/types'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; import type DotLottieAnimation from './LottieAnimations/types'; @@ -64,6 +65,9 @@ type BaseFeatureTrainingModalProps = { /** Secondary description rendered with additional space */ secondaryDescription?: string; + /** Style for the title */ + titleStyles?: StyleProp; + /** Whether to show `Don't show me this again` option */ shouldShowDismissModalOption?: boolean; @@ -120,6 +124,9 @@ type BaseFeatureTrainingModalProps = { /** Whether the user can confirm the tutorial while offline */ canConfirmWhileOffline?: boolean; + + /** Whether to navigate back when closing the modal */ + shouldGoBack?: boolean; }; type FeatureTrainingModalVideoProps = { @@ -141,10 +148,10 @@ type FeatureTrainingModalSVGProps = { contentFitImage?: ImageContentFit; /** The width of the image */ - imageWidth?: number; + imageWidth?: ImageSVGProps['width']; /** The height of the image */ - imageHeight?: number; + imageHeight?: ImageSVGProps['height']; }; // This page requires either an icon or a video/animation, but not both @@ -163,6 +170,7 @@ function FeatureTrainingModal({ title = '', description = '', secondaryDescription = '', + titleStyles, shouldShowDismissModalOption = false, confirmText = '', onConfirm = () => {}, @@ -183,6 +191,7 @@ function FeatureTrainingModal({ shouldUseScrollView = false, shouldShowConfirmationLoader = false, canConfirmWhileOffline = true, + shouldGoBack = true, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -319,10 +328,12 @@ function FeatureTrainingModal({ } setIsModalVisible(false); InteractionManager.runAfterInteractions(() => { - Navigation.goBack(); + if (shouldGoBack) { + Navigation.goBack(); + } onClose?.(); }); - }, [onClose, willShowAgain]); + }, [onClose, shouldGoBack, willShowAgain]); const closeAndConfirmModal = useCallback(() => { if (shouldCloseOnConfirm) { @@ -375,7 +386,7 @@ function FeatureTrainingModal({ {!!title && !!description && ( - {typeof title === 'string' ? {title} : title} + {typeof title === 'string' ? {title} : title} {shouldRenderHTMLDescription ? : {description}} {secondaryDescription.length > 0 && {secondaryDescription}} {children} diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts index 88d39f96cc9a0..765047830f952 100644 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -17,9 +17,10 @@ const { ACCOUNT_SWITCHER, EXPENSE_REPORTS_FILTER, SCAN_TEST_DRIVE_CONFIRMATION, + MULTI_SCAN_EDUCATIONAL_MODAL, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; -type ProductTrainingTooltipName = ValueOf; +type ProductTrainingTooltipName = Exclude, typeof MULTI_SCAN_EDUCATIONAL_MODAL>; type ShouldShowConditionProps = { shouldUseNarrowLayout: boolean; diff --git a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/languages/en.ts b/src/languages/en.ts index 158e6db92cef2..4231ecbed6ca3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -991,6 +991,8 @@ const translations = { one: 'Receipt scanning...', other: 'Receipts scanning...', }), + scanMultipleReceipts: 'Scan multiple receipts', + scanMultipleReceiptsDescription: 'Snap photos of all your receipts at once, then confirm details yourself or let SmartScan handle it.', receiptScanInProgress: 'Receipt scan in progress', receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.', duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) => diff --git a/src/languages/es.ts b/src/languages/es.ts index 41d942850da6e..32165b54b4104 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -991,6 +991,8 @@ const translations = { one: 'Escaneando recibo...', other: 'Escaneando recibos...', }), + scanMultipleReceipts: 'Escanea varios recibos', + scanMultipleReceiptsDescription: 'Tome fotos de todos sus recibos a la vez y confirme los detalles usted mismo o deje que SmartScan se encargue.', receiptScanInProgress: 'Escaneado de recibo en proceso', receiptScanInProgressDescription: 'Escaneado de recibo en proceso. Vuelve a comprobarlo más tarde o introduce los detalles ahora.', duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) => diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 5dd093b6a850d..f4ed982554c71 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -12,12 +12,14 @@ import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequ import type {Camera, PhotoFile, Point} from 'react-native-vision-camera'; import {useCameraDevice} from 'react-native-vision-camera'; import type {TupleToUnion} from 'type-fest'; +import MultiScan from '@assets/images/educational-illustration__multi-scan.svg'; import TestReceipt from '@assets/images/fake-receipt.png'; import Hand from '@assets/images/hand.svg'; import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Button from '@components/Button'; +import FeatureTrainingModal from '@components/FeatureTrainingModal'; import {useFullScreenLoader} from '@components/FullScreenLoaderContext'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -32,6 +34,7 @@ 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'; @@ -111,11 +114,13 @@ function IOURequestStepScan({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); const platform = getPlatform(true); const [mutedPlatforms = {}] = useOnyx(ONYXKEYS.NVP_MUTED_PLATFORMS, {canBeMissing: true}); const isPlatformMuted = mutedPlatforms[platform]; const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); + const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const [pdfFile, setPdfFile] = useState(null); @@ -540,6 +545,13 @@ function IOURequestStepScan({ }); }; + const dismissMultiScanEducationalPopup = () => { + InteractionManager.runAfterInteractions(() => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + setShouldShowMultiScanEducationalPopup(false); + }); + }; + /** * Sets the Receipt objects and navigates the user to the next page */ @@ -729,6 +741,9 @@ function IOURequestStepScan({ ]); const toggleMultiScan = () => { + if (!dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]) { + setShouldShowMultiScanEducationalPopup(true); + } if (isMultiScanEnabled) { removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); removeDraftTransactions(true); @@ -848,7 +863,21 @@ function IOURequestStepScan({ )} - + {shouldShowMultiScanEducationalPopup && ( + + )} setIsLoaderVisible(true)}> {({openPicker}) => ( diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 4a88e79cbf5f3..a8173c07375c3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -2,12 +2,13 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; import {Str} from 'expensify-common'; import React, {useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import {ActivityIndicator, PanResponder, PixelRatio, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, InteractionManager, PanResponder, PixelRatio, StyleSheet, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import {RESULTS} from 'react-native-permissions'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import type Webcam from 'react-webcam'; +import MultiScan from '@assets/images/educational-illustration__multi-scan.svg'; import TestReceipt from '@assets/images/fake-receipt.png'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; @@ -21,6 +22,7 @@ import DownloadAppBanner from '@components/DownloadAppBanner'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; +import FeatureTrainingModal from '@components/FeatureTrainingModal'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -38,6 +40,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import setTestReceipt from '@libs/actions/setTestReceipt'; import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation'; +import {dismissProductTraining} from '@libs/actions/Welcome'; import {isMobile, isMobileWebKit} from '@libs/Browser'; import {base64ToFile, isLocalFile as isLocalFileFileUtils, resizeImageIfNeeded, validateReceipt} from '@libs/fileDownload/FileUtils'; import convertHeicImage from '@libs/fileDownload/heicConverter'; @@ -119,6 +122,7 @@ function IOURequestStepScan({ const cameraRef = useRef(null); const trackRef = useRef(null); const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false); + const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const getScreenshotTimeoutRef = useRef(null); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); @@ -127,6 +131,7 @@ function IOURequestStepScan({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); const isEditing = action === CONST.IOU.ACTION.EDIT; // TODO: use correct canUseMultiScan value when all multi-scan functionality is implemented @@ -754,6 +759,9 @@ function IOURequestStepScan({ ]); const toggleMultiScan = () => { + if (!dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]) { + setShouldShowMultiScanEducationalPopup(true); + } if (isMultiScanEnabled) { removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID); removeDraftTransactions(true); @@ -831,6 +839,13 @@ function IOURequestStepScan({ return translate(attachmentInvalidReason); }; + const dismissMultiScanEducationalPopup = () => { + InteractionManager.runAfterInteractions(() => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + setShouldShowMultiScanEducationalPopup(false); + }); + }; + const mobileCameraView = () => ( <> @@ -977,7 +992,22 @@ function IOURequestStepScan({ )} - + {canUseMultiScan && isMobile() && shouldShowMultiScanEducationalPopup && ( + + )} thumbnailImageContainerHighlight: { backgroundColor: theme.highlightBG, }, + + multiScanEducationalPopupImage: { + backgroundColor: colors.pink700, + overflow: 'hidden', + paddingHorizontal: 0, + aspectRatio: 1.7, + }, }) satisfies Styles; type ThemeStyles = ReturnType; diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index 4fe066abb5548..e00c81e805c83 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -14,6 +14,7 @@ const { GBR_RBR_CHAT, EXPENSE_REPORTS_FILTER, SCAN_TEST_DRIVE_CONFIRMATION, + MULTI_SCAN_EDUCATIONAL_MODAL, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; /** @@ -102,6 +103,11 @@ type DismissedProductTraining = { */ [SCAN_TEST_DRIVE_CONFIRMATION]: DismissedProductTrainingElement; + /** + * When user dismisses the MultiScan product training tooltip, we store the timestamp here. + */ + [MULTI_SCAN_EDUCATIONAL_MODAL]: DismissedProductTrainingElement; + /** * When user dismisses the ChangeReportPolicy feature training modal, we store the timestamp here. */