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.
*/