Skip to content

Commit 4764b6b

Browse files
authored
Merge pull request Expensify#63451 from callstack-internal/feat/61180-multi-scan-educational-popup
Multi-Scan Educational Pop-up
2 parents a3e39d2 + 1d903ac commit 4764b6b

11 files changed

Lines changed: 479 additions & 10 deletions

File tree

assets/images/educational-illustration__multi-scan.svg

Lines changed: 380 additions & 0 deletions
Loading

src/CONST.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7126,6 +7126,7 @@ const CONST = {
71267126
ACCOUNT_SWITCHER: 'accountSwitcher',
71277127
EXPENSE_REPORTS_FILTER: 'expenseReportsFilter',
71287128
SCAN_TEST_DRIVE_CONFIRMATION: 'scanTestDriveConfirmation',
7129+
MULTI_SCAN_EDUCATIONAL_MODAL: 'multiScanEducationalModal',
71297130
},
71307131
CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal',
71317132
SMART_BANNER_HEIGHT: 152,

src/components/FeatureTrainingModal.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {VideoReadyForDisplayEvent} from 'expo-av';
22
import type {ImageContentFit} from 'expo-image';
33
import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
44
import {Image, InteractionManager, View} from 'react-native';
5-
import type {ImageResizeMode, ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
5+
import type {ImageResizeMode, ImageSourcePropType, StyleProp, TextStyle, ViewStyle} from 'react-native';
66
import {GestureHandlerRootView} from 'react-native-gesture-handler';
77
import type {MergeExclusive} from 'type-fest';
88
import useLocalize from '@hooks/useLocalize';
@@ -20,6 +20,7 @@ import Button from './Button';
2020
import CheckboxWithLabel from './CheckboxWithLabel';
2121
import FormAlertWithSubmitButton from './FormAlertWithSubmitButton';
2222
import ImageSVG from './ImageSVG';
23+
import type ImageSVGProps from './ImageSVG/types';
2324
import Lottie from './Lottie';
2425
import LottieAnimations from './LottieAnimations';
2526
import type DotLottieAnimation from './LottieAnimations/types';
@@ -64,6 +65,9 @@ type BaseFeatureTrainingModalProps = {
6465
/** Secondary description rendered with additional space */
6566
secondaryDescription?: string;
6667

68+
/** Style for the title */
69+
titleStyles?: StyleProp<TextStyle>;
70+
6771
/** Whether to show `Don't show me this again` option */
6872
shouldShowDismissModalOption?: boolean;
6973

@@ -120,6 +124,9 @@ type BaseFeatureTrainingModalProps = {
120124

121125
/** Whether the user can confirm the tutorial while offline */
122126
canConfirmWhileOffline?: boolean;
127+
128+
/** Whether to navigate back when closing the modal */
129+
shouldGoBack?: boolean;
123130
};
124131

125132
type FeatureTrainingModalVideoProps = {
@@ -141,10 +148,10 @@ type FeatureTrainingModalSVGProps = {
141148
contentFitImage?: ImageContentFit;
142149

143150
/** The width of the image */
144-
imageWidth?: number;
151+
imageWidth?: ImageSVGProps['width'];
145152

146153
/** The height of the image */
147-
imageHeight?: number;
154+
imageHeight?: ImageSVGProps['height'];
148155
};
149156

150157
// This page requires either an icon or a video/animation, but not both
@@ -163,6 +170,7 @@ function FeatureTrainingModal({
163170
title = '',
164171
description = '',
165172
secondaryDescription = '',
173+
titleStyles,
166174
shouldShowDismissModalOption = false,
167175
confirmText = '',
168176
onConfirm = () => {},
@@ -183,6 +191,7 @@ function FeatureTrainingModal({
183191
shouldUseScrollView = false,
184192
shouldShowConfirmationLoader = false,
185193
canConfirmWhileOffline = true,
194+
shouldGoBack = true,
186195
}: FeatureTrainingModalProps) {
187196
const styles = useThemeStyles();
188197
const StyleUtils = useStyleUtils();
@@ -319,10 +328,12 @@ function FeatureTrainingModal({
319328
}
320329
setIsModalVisible(false);
321330
InteractionManager.runAfterInteractions(() => {
322-
Navigation.goBack();
331+
if (shouldGoBack) {
332+
Navigation.goBack();
333+
}
323334
onClose?.();
324335
});
325-
}, [onClose, willShowAgain]);
336+
}, [onClose, shouldGoBack, willShowAgain]);
326337

327338
const closeAndConfirmModal = useCallback(() => {
328339
if (shouldCloseOnConfirm) {
@@ -375,7 +386,7 @@ function FeatureTrainingModal({
375386
<View style={[styles.mt5, styles.mh5, contentOuterContainerStyles]}>
376387
{!!title && !!description && (
377388
<View style={[onboardingIsMediumOrLargerScreenWidth ? [styles.gap1, styles.mb8] : [styles.mb10], contentInnerContainerStyles]}>
378-
{typeof title === 'string' ? <Text style={[styles.textHeadlineH1]}>{title}</Text> : title}
389+
{typeof title === 'string' ? <Text style={[styles.textHeadlineH1, titleStyles]}>{title}</Text> : title}
379390
{shouldRenderHTMLDescription ? <RenderHTML html={description} /> : <Text style={styles.textSupporting}>{description}</Text>}
380391
{secondaryDescription.length > 0 && <Text style={[styles.textSupporting, styles.mt4]}>{secondaryDescription}</Text>}
381392
{children}

src/components/ProductTrainingContext/TOOLTIPS.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ const {
1717
ACCOUNT_SWITCHER,
1818
EXPENSE_REPORTS_FILTER,
1919
SCAN_TEST_DRIVE_CONFIRMATION,
20+
MULTI_SCAN_EDUCATIONAL_MODAL,
2021
} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;
2122

22-
type ProductTrainingTooltipName = ValueOf<typeof CONST.PRODUCT_TRAINING_TOOLTIP_NAMES>;
23+
type ProductTrainingTooltipName = Exclude<ValueOf<typeof CONST.PRODUCT_TRAINING_TOOLTIP_NAMES>, typeof MULTI_SCAN_EDUCATIONAL_MODAL>;
2324

2425
type ShouldShowConditionProps = {
2526
shouldUseNarrowLayout: boolean;

src/components/Search/SearchPageHeader/SearchStatusBar.tsx

Whitespace-only changes.

src/languages/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,8 @@ const translations = {
991991
one: 'Receipt scanning...',
992992
other: 'Receipts scanning...',
993993
}),
994+
scanMultipleReceipts: 'Scan multiple receipts',
995+
scanMultipleReceiptsDescription: 'Snap photos of all your receipts at once, then confirm details yourself or let SmartScan handle it.',
994996
receiptScanInProgress: 'Receipt scan in progress',
995997
receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.',
996998
duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) =>

src/languages/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,8 @@ const translations = {
991991
one: 'Escaneando recibo...',
992992
other: 'Escaneando recibos...',
993993
}),
994+
scanMultipleReceipts: 'Escanea varios recibos',
995+
scanMultipleReceiptsDescription: 'Tome fotos de todos sus recibos a la vez y confirme los detalles usted mismo o deje que SmartScan se encargue.',
994996
receiptScanInProgress: 'Escaneado de recibo en proceso',
995997
receiptScanInProgressDescription: 'Escaneado de recibo en proceso. Vuelve a comprobarlo más tarde o introduce los detalles ahora.',
996998
duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) =>

src/pages/iou/request/step/IOURequestStepScan/index.native.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequ
1212
import type {Camera, PhotoFile, Point} from 'react-native-vision-camera';
1313
import {useCameraDevice} from 'react-native-vision-camera';
1414
import type {TupleToUnion} from 'type-fest';
15+
import MultiScan from '@assets/images/educational-illustration__multi-scan.svg';
1516
import TestReceipt from '@assets/images/fake-receipt.png';
1617
import Hand from '@assets/images/hand.svg';
1718
import Shutter from '@assets/images/shutter.svg';
1819
import type {FileObject} from '@components/AttachmentModal';
1920
import AttachmentPicker from '@components/AttachmentPicker';
2021
import Button from '@components/Button';
22+
import FeatureTrainingModal from '@components/FeatureTrainingModal';
2123
import {useFullScreenLoader} from '@components/FullScreenLoaderContext';
2224
import Icon from '@components/Icon';
2325
import * as Expensicons from '@components/Icon/Expensicons';
@@ -32,6 +34,7 @@ import usePolicy from '@hooks/usePolicy';
3234
import useTheme from '@hooks/useTheme';
3335
import useThemeStyles from '@hooks/useThemeStyles';
3436
import setTestReceipt from '@libs/actions/setTestReceipt';
37+
import {dismissProductTraining} from '@libs/actions/Welcome';
3538
import {readFileAsync, resizeImageIfNeeded, showCameraPermissionsAlert, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils';
3639
import getPhotoSource from '@libs/fileDownload/getPhotoSource';
3740
import convertHeicImage from '@libs/fileDownload/heicConverter';
@@ -111,11 +114,13 @@ function IOURequestStepScan({
111114
const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true});
112115
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false});
113116
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true});
117+
const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
114118
const platform = getPlatform(true);
115119
const [mutedPlatforms = {}] = useOnyx(ONYXKEYS.NVP_MUTED_PLATFORMS, {canBeMissing: true});
116120
const isPlatformMuted = mutedPlatforms[platform];
117121
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<string | null>(null);
118122
const [didCapturePhoto, setDidCapturePhoto] = useState(false);
123+
const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false);
119124

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

@@ -540,6 +545,13 @@ function IOURequestStepScan({
540545
});
541546
};
542547

548+
const dismissMultiScanEducationalPopup = () => {
549+
InteractionManager.runAfterInteractions(() => {
550+
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL);
551+
setShouldShowMultiScanEducationalPopup(false);
552+
});
553+
};
554+
543555
/**
544556
* Sets the Receipt objects and navigates the user to the next page
545557
*/
@@ -729,6 +741,9 @@ function IOURequestStepScan({
729741
]);
730742

731743
const toggleMultiScan = () => {
744+
if (!dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]) {
745+
setShouldShowMultiScanEducationalPopup(true);
746+
}
732747
if (isMultiScanEnabled) {
733748
removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID);
734749
removeDraftTransactions(true);
@@ -848,7 +863,21 @@ function IOURequestStepScan({
848863
</View>
849864
)}
850865
</View>
851-
866+
{shouldShowMultiScanEducationalPopup && (
867+
<FeatureTrainingModal
868+
title={translate('iou.scanMultipleReceipts')}
869+
image={MultiScan}
870+
shouldRenderSVG
871+
imageHeight={220}
872+
modalInnerContainerStyle={styles.pt0}
873+
illustrationOuterContainerStyle={styles.multiScanEducationalPopupImage}
874+
onConfirm={dismissMultiScanEducationalPopup}
875+
titleStyles={styles.mb2}
876+
confirmText={translate('common.buttonConfirm')}
877+
description={translate('iou.scanMultipleReceiptsDescription')}
878+
shouldGoBack={false}
879+
/>
880+
)}
852881
<View style={[styles.flexRow, styles.justifyContentAround, styles.alignItemsCenter, styles.pv3]}>
853882
<AttachmentPicker onOpenPicker={() => setIsLoaderVisible(true)}>
854883
{({openPicker}) => (

src/pages/iou/request/step/IOURequestStepScan/index.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import {useIsFocused} from '@react-navigation/native';
22
import {format} from 'date-fns';
33
import {Str} from 'expensify-common';
44
import React, {useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState} from 'react';
5-
import {ActivityIndicator, PanResponder, PixelRatio, StyleSheet, View} from 'react-native';
5+
import {ActivityIndicator, InteractionManager, PanResponder, PixelRatio, StyleSheet, View} from 'react-native';
66
import type {OnyxEntry} from 'react-native-onyx';
77
import {useOnyx} from 'react-native-onyx';
88
import {RESULTS} from 'react-native-permissions';
99
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
1010
import type Webcam from 'react-webcam';
11+
import MultiScan from '@assets/images/educational-illustration__multi-scan.svg';
1112
import TestReceipt from '@assets/images/fake-receipt.png';
1213
import Hand from '@assets/images/hand.svg';
1314
import ReceiptUpload from '@assets/images/receipt-upload.svg';
@@ -21,6 +22,7 @@ import DownloadAppBanner from '@components/DownloadAppBanner';
2122
import DragAndDropConsumer from '@components/DragAndDrop/Consumer';
2223
import {DragAndDropContext} from '@components/DragAndDrop/Provider';
2324
import DropZoneUI from '@components/DropZone/DropZoneUI';
25+
import FeatureTrainingModal from '@components/FeatureTrainingModal';
2426
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
2527
import Icon from '@components/Icon';
2628
import * as Expensicons from '@components/Icon/Expensicons';
@@ -38,6 +40,7 @@ import useTheme from '@hooks/useTheme';
3840
import useThemeStyles from '@hooks/useThemeStyles';
3941
import setTestReceipt from '@libs/actions/setTestReceipt';
4042
import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation';
43+
import {dismissProductTraining} from '@libs/actions/Welcome';
4144
import {isMobile, isMobileWebKit} from '@libs/Browser';
4245
import {base64ToFile, isLocalFile as isLocalFileFileUtils, resizeImageIfNeeded, validateReceipt} from '@libs/fileDownload/FileUtils';
4346
import convertHeicImage from '@libs/fileDownload/heicConverter';
@@ -119,6 +122,7 @@ function IOURequestStepScan({
119122
const cameraRef = useRef<Webcam>(null);
120123
const trackRef = useRef<MediaStreamTrack | null>(null);
121124
const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false);
125+
const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false);
122126

123127
const getScreenshotTimeoutRef = useRef<NodeJS.Timeout | null>(null);
124128
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
@@ -127,6 +131,7 @@ function IOURequestStepScan({
127131
const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true});
128132
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false});
129133
const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true});
134+
const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true});
130135
const [isLoadingReceipt, setIsLoadingReceipt] = useState(false);
131136
const isEditing = action === CONST.IOU.ACTION.EDIT;
132137
// TODO: use correct canUseMultiScan value when all multi-scan functionality is implemented
@@ -754,6 +759,9 @@ function IOURequestStepScan({
754759
]);
755760

756761
const toggleMultiScan = () => {
762+
if (!dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]) {
763+
setShouldShowMultiScanEducationalPopup(true);
764+
}
757765
if (isMultiScanEnabled) {
758766
removeTransactionReceipt(CONST.IOU.OPTIMISTIC_TRANSACTION_ID);
759767
removeDraftTransactions(true);
@@ -831,6 +839,13 @@ function IOURequestStepScan({
831839
return translate(attachmentInvalidReason);
832840
};
833841

842+
const dismissMultiScanEducationalPopup = () => {
843+
InteractionManager.runAfterInteractions(() => {
844+
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL);
845+
setShouldShowMultiScanEducationalPopup(false);
846+
});
847+
};
848+
834849
const mobileCameraView = () => (
835850
<>
836851
<View style={[styles.cameraView]}>
@@ -977,7 +992,22 @@ function IOURequestStepScan({
977992
</PressableWithFeedback>
978993
)}
979994
</View>
980-
995+
{canUseMultiScan && isMobile() && shouldShowMultiScanEducationalPopup && (
996+
<FeatureTrainingModal
997+
title={translate('iou.scanMultipleReceipts')}
998+
image={MultiScan}
999+
shouldRenderSVG
1000+
imageHeight="auto"
1001+
imageWidth="auto"
1002+
modalInnerContainerStyle={styles.pt0}
1003+
illustrationOuterContainerStyle={styles.multiScanEducationalPopupImage}
1004+
onConfirm={dismissMultiScanEducationalPopup}
1005+
titleStyles={styles.mb2}
1006+
confirmText={translate('common.buttonConfirm')}
1007+
description={translate('iou.scanMultipleReceiptsDescription')}
1008+
shouldGoBack={false}
1009+
/>
1010+
)}
9811011
<ReceiptPreviews
9821012
isMultiScanEnabled={isMultiScanEnabled}
9831013
submit={submitReceipts}

src/styles/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5904,6 +5904,13 @@ const styles = (theme: ThemeColors) =>
59045904
thumbnailImageContainerHighlight: {
59055905
backgroundColor: theme.highlightBG,
59065906
},
5907+
5908+
multiScanEducationalPopupImage: {
5909+
backgroundColor: colors.pink700,
5910+
overflow: 'hidden',
5911+
paddingHorizontal: 0,
5912+
aspectRatio: 1.7,
5913+
},
59075914
}) satisfies Styles;
59085915

59095916
type ThemeStyles = ReturnType<typeof styles>;

0 commit comments

Comments
 (0)