Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
380 changes: 380 additions & 0 deletions assets/images/educational-illustration__multi-scan.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@
* @deprecated
* This will be fully cleaned up in https://github.com/Expensify/App/issues/63254
* */
TABLE_REPORT_VIEW: 'tableReportView',

Check failure on line 827 in src/CONST.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'TABLE_REPORT_VIEW' is deprecated. This will be fully cleaned up in https://github.com/Expensify/App/issues/63254
WALLET: 'newdotWallet',
GLOBAL_REIMBURSEMENTS_ON_ND: 'globalReimbursementsOnND',
RETRACT_NEWDOT: 'retractNewDot',
Expand Down Expand Up @@ -7126,6 +7126,7 @@
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,
Expand Down
23 changes: 17 additions & 6 deletions src/components/FeatureTrainingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -64,6 +65,9 @@ type BaseFeatureTrainingModalProps = {
/** Secondary description rendered with additional space */
secondaryDescription?: string;

/** Style for the title */
titleStyles?: StyleProp<TextStyle>;

/** Whether to show `Don't show me this again` option */
shouldShowDismissModalOption?: boolean;

Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -163,6 +170,7 @@ function FeatureTrainingModal({
title = '',
description = '',
secondaryDescription = '',
titleStyles,
shouldShowDismissModalOption = false,
confirmText = '',
onConfirm = () => {},
Expand All @@ -183,6 +191,7 @@ function FeatureTrainingModal({
shouldUseScrollView = false,
shouldShowConfirmationLoader = false,
canConfirmWhileOffline = true,
shouldGoBack = true,
}: FeatureTrainingModalProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -375,7 +386,7 @@ function FeatureTrainingModal({
<View style={[styles.mt5, styles.mh5, contentOuterContainerStyles]}>
{!!title && !!description && (
<View style={[onboardingIsMediumOrLargerScreenWidth ? [styles.gap1, styles.mb8] : [styles.mb10], contentInnerContainerStyles]}>
{typeof title === 'string' ? <Text style={[styles.textHeadlineH1]}>{title}</Text> : title}
{typeof title === 'string' ? <Text style={[styles.textHeadlineH1, titleStyles]}>{title}</Text> : title}
{shouldRenderHTMLDescription ? <RenderHTML html={description} /> : <Text style={styles.textSupporting}>{description}</Text>}
{secondaryDescription.length > 0 && <Text style={[styles.textSupporting, styles.mt4]}>{secondaryDescription}</Text>}
{children}
Expand Down
3 changes: 2 additions & 1 deletion src/components/ProductTrainingContext/TOOLTIPS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof CONST.PRODUCT_TRAINING_TOOLTIP_NAMES>;
type ProductTrainingTooltipName = Exclude<ValueOf<typeof CONST.PRODUCT_TRAINING_TOOLTIP_NAMES>, typeof MULTI_SCAN_EDUCATIONAL_MODAL>;

type ShouldShowConditionProps = {
shouldUseNarrowLayout: boolean;
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
31 changes: 30 additions & 1 deletion src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<string | null>(null);
const [didCapturePhoto, setDidCapturePhoto] = useState(false);
const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false);

const [pdfFile, setPdfFile] = useState<null | FileObject>(null);

Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -848,7 +863,21 @@ function IOURequestStepScan({
</View>
)}
</View>

{shouldShowMultiScanEducationalPopup && (
<FeatureTrainingModal
title={translate('iou.scanMultipleReceipts')}
image={MultiScan}
shouldRenderSVG
imageHeight={220}
modalInnerContainerStyle={styles.pt0}
illustrationOuterContainerStyle={styles.multiScanEducationalPopupImage}
onConfirm={dismissMultiScanEducationalPopup}
titleStyles={styles.mb2}
confirmText={translate('common.buttonConfirm')}
description={translate('iou.scanMultipleReceiptsDescription')}
shouldGoBack={false}
/>
)}
<View style={[styles.flexRow, styles.justifyContentAround, styles.alignItemsCenter, styles.pv3]}>
<AttachmentPicker onOpenPicker={() => setIsLoaderVisible(true)}>
{({openPicker}) => (
Expand Down
34 changes: 32 additions & 2 deletions src/pages/iou/request/step/IOURequestStepScan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -119,6 +122,7 @@ function IOURequestStepScan({
const cameraRef = useRef<Webcam>(null);
const trackRef = useRef<MediaStreamTrack | null>(null);
const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false);
const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false);

const getScreenshotTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 = () => (
<>
<View style={[styles.cameraView]}>
Expand Down Expand Up @@ -977,7 +992,22 @@ function IOURequestStepScan({
</PressableWithFeedback>
)}
</View>

{canUseMultiScan && isMobile() && shouldShowMultiScanEducationalPopup && (
<FeatureTrainingModal
title={translate('iou.scanMultipleReceipts')}
image={MultiScan}
shouldRenderSVG
imageHeight="auto"
imageWidth="auto"
modalInnerContainerStyle={styles.pt0}
illustrationOuterContainerStyle={styles.multiScanEducationalPopupImage}
onConfirm={dismissMultiScanEducationalPopup}
titleStyles={styles.mb2}
confirmText={translate('common.buttonConfirm')}
description={translate('iou.scanMultipleReceiptsDescription')}
shouldGoBack={false}
/>
)}
<ReceiptPreviews
isMultiScanEnabled={isMultiScanEnabled}
submit={submitReceipts}
Expand Down
7 changes: 7 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5904,6 +5904,13 @@ const styles = (theme: ThemeColors) =>
thumbnailImageContainerHighlight: {
backgroundColor: theme.highlightBG,
},

multiScanEducationalPopupImage: {
backgroundColor: colors.pink700,
overflow: 'hidden',
paddingHorizontal: 0,
aspectRatio: 1.7,
},
}) satisfies Styles;

type ThemeStyles = ReturnType<typeof styles>;
Expand Down
6 changes: 6 additions & 0 deletions src/types/onyx/DismissedProductTraining.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
GBR_RBR_CHAT,
EXPENSE_REPORTS_FILTER,
SCAN_TEST_DRIVE_CONFIRMATION,
MULTI_SCAN_EDUCATIONAL_MODAL,
} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading