From 937c98fecdafcab42d729dce17cdfb3dc8d8a41b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 16 May 2025 19:01:53 +0200 Subject: [PATCH 01/11] feat: add educational popup --- assets/images/multi-scan-hand.svg | 242 ++++++++++++++++++ src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../step/IOURequestStepScan/index.native.tsx | 18 +- 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 assets/images/multi-scan-hand.svg diff --git a/assets/images/multi-scan-hand.svg b/assets/images/multi-scan-hand.svg new file mode 100644 index 0000000000000..0ae6365b9cefe --- /dev/null +++ b/assets/images/multi-scan-hand.svg @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/languages/en.ts b/src/languages/en.ts index 735eeb0f33ce6..b2dc478c2bc9b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -962,6 +962,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 a348dbf381f31..cdb6de8866851 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -962,6 +962,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 9f6047f940408..932eac695e1b6 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -12,10 +12,12 @@ import {useCameraDevice} from 'react-native-vision-camera'; import type {TupleToUnion} from 'type-fest'; import TestReceipt from '@assets/images/fake-receipt.png'; import Hand from '@assets/images/hand.svg'; +import MultiScanHand from '@assets/images/multi-scan-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 ConfirmModal from '@components/ConfirmModal'; import {useFullScreenLoader} from '@components/FullScreenLoaderContext'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -50,6 +52,7 @@ 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 colors from '@styles/theme/colors'; import variables from '@styles/variables'; import { getMoneyRequestParticipantsFromReport, @@ -784,7 +787,20 @@ function IOURequestStepScan({ )} - + {}} + onCancel={() => {}} + confirmText={translate('common.buttonConfirm')} + prompt={translate('iou.scanMultipleReceiptsDescription')} + promptStyles={[styles.textSupporting, styles.mb2]} + titleStyles={styles.textHeadline} + titleContainerStyles={styles.mb3} + shouldShowCancelButton={false} + /> setIsLoaderVisible(true)}> {({openPicker}) => ( From 75a10338838f3d765d54179b0f97ac2316f41444 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 May 2025 17:30:10 +0200 Subject: [PATCH 02/11] fix: change ConfirmModal for FeatureTrainingModal --- src/components/FeatureTrainingModal.tsx | 18 +++++++++++---- .../step/IOURequestStepScan/index.native.tsx | 22 ++++++++++--------- src/styles/index.ts | 6 +++++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index cc3b7f71e826b..79f0c0ef00f88 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, ViewStyle, TextStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import type {MergeExclusive} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; @@ -60,6 +60,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; @@ -98,6 +101,9 @@ type BaseFeatureTrainingModalProps = { /** Whether the modal image is a SVG */ shouldRenderSVG?: boolean; + + /** Whether to navigate back when closing the modal */ + shouldGoBack?: boolean; }; type FeatureTrainingModalVideoProps = { @@ -141,6 +147,7 @@ function FeatureTrainingModal({ title = '', description = '', secondaryDescription = '', + titleStyles, shouldShowDismissModalOption = false, confirmText = '', onConfirm = () => {}, @@ -155,6 +162,7 @@ function FeatureTrainingModal({ imageHeight, isModalDisabled = true, shouldRenderSVG = true, + shouldGoBack = true, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -291,10 +299,12 @@ function FeatureTrainingModal({ } setIsModalVisible(false); InteractionManager.runAfterInteractions(() => { - Navigation.goBack(); + if (shouldGoBack) { + Navigation.goBack(); + } onClose?.(); }); - }, [onClose, willShowAgain]); + }, [onClose, shouldGoBack, willShowAgain]); const closeAndConfirmModal = useCallback(() => { closeModal(); @@ -341,7 +351,7 @@ function FeatureTrainingModal({ {!!title && !!description && ( - {typeof title === 'string' ? {title} : title} + {typeof title === 'string' ? {title} : title} {description} {secondaryDescription.length > 0 && {secondaryDescription}} {children} diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 932eac695e1b6..0a0e6c242ef9c 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -17,7 +17,7 @@ import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Button from '@components/Button'; -import ConfirmModal from '@components/ConfirmModal'; +import FeatureTrainingModal from '@components/FeatureTrainingModal'; import {useFullScreenLoader} from '@components/FullScreenLoaderContext'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -787,19 +787,21 @@ function IOURequestStepScan({ )} - {}} - onCancel={() => {}} + onClose={() => {}} + titleStyles={styles.mb2} confirmText={translate('common.buttonConfirm')} - prompt={translate('iou.scanMultipleReceiptsDescription')} - promptStyles={[styles.textSupporting, styles.mb2]} - titleStyles={styles.textHeadline} - titleContainerStyles={styles.mb3} - shouldShowCancelButton={false} + description={translate('iou.scanMultipleReceiptsDescription')} + shouldGoBack={false} /> setIsLoaderVisible(true)}> diff --git a/src/styles/index.ts b/src/styles/index.ts index 94df3017ee772..ea7562965e0a3 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5773,6 +5773,12 @@ const styles = (theme: ThemeColors) => // Choosing a lowest value just above the threshold for the items to adjust width against the various screens. Only 2 items are shown 35 * 2 = 70 thus third item of 35% width can't fit forcing a two column layout. flexBasis: '35%', }, + + multiScanEducationalPopupImage: { + backgroundColor: colors.pink700, + overflow: 'hidden', + height: 220, + } } satisfies Styles); type ThemeStyles = ReturnType; From 5cfe5693d1884f2da1b51f6b6abba3ee4b9e3050 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 May 2025 19:15:00 +0200 Subject: [PATCH 03/11] feat: add to const --- src/CONST.ts | 3 ++- src/components/ProductTrainingContext/TOOLTIPS.ts | 14 +++++++++----- .../Search/SearchPageHeader/SearchStatusBar.tsx | 2 +- src/types/onyx/DismissedProductTraining.ts | 10 ++++++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 4d7049271e787..e14fe1ab1f4be 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -7031,11 +7031,12 @@ const CONST = { SCAN_TEST_TOOLTIP: 'scanTestTooltip', SCAN_TEST_TOOLTIP_MANAGER: 'scanTestTooltipManager', SCAN_TEST_CONFIRMATION: 'scanTestConfirmation', - OUTSANDING_FILTER: 'outstandingFilter', + OUTSTANDING_FILTER: 'outstandingFilter', WORKSPACES_SETTINGS: 'workspacesSettings', GBR_RBR_CHAT: 'chatGBRRBR', ACCOUNT_SWITCHER: 'accountSwitcher', EXPENSE_REPORTS_FILTER: 'expenseReportsFilter', + MULTI_SCAN_EDUCATIONAL_MODAL: 'multiScanEducationalModal' }, CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', SMART_BANNER_HEIGHT: 152, diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts index 81f7fffce76eb..470894c9580f7 100644 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -12,14 +12,18 @@ const { SCAN_TEST_TOOLTIP, SCAN_TEST_TOOLTIP_MANAGER, SCAN_TEST_CONFIRMATION, - OUTSANDING_FILTER, + OUTSTANDING_FILTER, WORKSPACES_SETTINGS, GBR_RBR_CHAT, ACCOUNT_SWITCHER, EXPENSE_REPORTS_FILTER, + MULTI_SCAN_EDUCATIONAL_MODAL, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; -type ProductTrainingTooltipName = ValueOf; +type ProductTrainingTooltipName = Exclude< + ValueOf, + typeof MULTI_SCAN_EDUCATIONAL_MODAL +>; type ShouldShowConditionProps = { shouldUseNarrowLayout: boolean; @@ -162,13 +166,13 @@ const TOOLTIPS: Record = { priority: 1100, shouldShow: () => true, }, - [OUTSANDING_FILTER]: { + [OUTSTANDING_FILTER]: { content: [ {text: 'productTrainingTooltip.outstandingFilter.part1', isBold: false}, {text: 'productTrainingTooltip.outstandingFilter.part2', isBold: true}, ], - onHideTooltip: () => dismissProductTraining(OUTSANDING_FILTER), - name: OUTSANDING_FILTER, + onHideTooltip: () => dismissProductTraining(OUTSTANDING_FILTER), + name: OUTSTANDING_FILTER, priority: 1925, shouldShow: ({isUserPolicyAdmin}) => isUserPolicyAdmin, }, diff --git a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx index 08c1f548df777..02c89f88e0635 100644 --- a/src/components/Search/SearchPageHeader/SearchStatusBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchStatusBar.tsx @@ -270,7 +270,7 @@ function SearchStatusBar({queryJSON, onStatusChange, headerButtonsOptions}: Sear ? queryJSON.status.includes(CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING) : queryJSON.status === CONST.SEARCH.STATUS.EXPENSE.OUTSTANDING; const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.OUTSANDING_FILTER, + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.OUTSTANDING_FILTER, isScreenFocused && !isOutstandingStatusActive && queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE, ); // Controls the visibility of the educational tooltip based on user scrolling. diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index 69a56d6f31199..c3171df12f891 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -9,11 +9,12 @@ const { SCAN_TEST_TOOLTIP, SCAN_TEST_TOOLTIP_MANAGER, SCAN_TEST_CONFIRMATION, - OUTSANDING_FILTER, + OUTSTANDING_FILTER, WORKSPACES_SETTINGS, ACCOUNT_SWITCHER, GBR_RBR_CHAT, EXPENSE_REPORTS_FILTER, + MULTI_SCAN_EDUCATIONAL_MODAL, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; /** @@ -80,7 +81,7 @@ type DismissedProductTraining = { /** * When user dismisses the outstanding filter product training tooltip, we store the timestamp here. */ - [OUTSANDING_FILTER]: DismissedProductTrainingElement; + [OUTSTANDING_FILTER]: DismissedProductTrainingElement; /** * When user dismisses the workspaces settings product training tooltip, we store the timestamp here. @@ -102,6 +103,11 @@ type DismissedProductTraining = { */ [EXPENSE_REPORTS_FILTER]: 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. */ From 3b528f0e2f6f73c7801645f07a4f68ee41b5f11e Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 19 May 2025 19:55:59 +0200 Subject: [PATCH 04/11] feat: dismiss modal function, show the modal when no dismissedProductTraining --- .../step/IOURequestStepScan/index.native.tsx | 74 ++++++++++++++----- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 2aae041fa7795..264f6f2a1b681 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -112,6 +112,7 @@ function IOURequestStepScan({ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: false}); + 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]; @@ -119,6 +120,8 @@ function IOURequestStepScan({ const [didCapturePhoto, setDidCapturePhoto] = useState(false); const isTabActive = useIsFocused(); + const shouldShowMultiScanEducationalPopup = !dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]; + const [pdfFile, setPdfFile] = useState(null); const defaultTaxCode = getDefaultTaxCode(policy, initialTransaction); @@ -287,7 +290,15 @@ function IOURequestStepScan({ ); const createTransaction = useCallback( - (files: ReceiptFile[], participant: Participant, gpsPoints?: GpsPoint, policyParams?: {policy: OnyxEntry}, billable?: boolean) => { + ( + files: ReceiptFile[], + participant: Participant, + gpsPoints?: GpsPoint, + policyParams?: { + policy: OnyxEntry; + }, + billable?: boolean, + ) => { files.forEach((receiptFile: ReceiptFile, index) => { const transaction = transactions.find((item) => item.transactionID === receiptFile.transactionID); const receipt: Receipt = receiptFile.file; @@ -374,7 +385,17 @@ function IOURequestStepScan({ if (!managerMcTestParticipant.reportID && report?.reportID) { reportIDParam = generateReportID(); } - setMoneyRequestParticipants(initialTransactionID, [{...managerMcTestParticipant, reportID: reportIDParam, selected: true}], true).then(() => { + setMoneyRequestParticipants( + initialTransactionID, + [ + { + ...managerMcTestParticipant, + reportID: reportIDParam, + selected: true, + }, + ], + true, + ).then(() => { navigateToConfirmationPage(true, reportIDParam); }); return; @@ -530,7 +551,17 @@ function IOURequestStepScan({ } setMoneyRequestReceipt(initialTransactionID, file.uri, filename, !isEditing, file.type, true); - navigateToConfirmationStep([{file, source: file.uri, transactionID: initialTransactionID}], false, true); + navigateToConfirmationStep( + [ + { + file, + source: file.uri, + transactionID: initialTransactionID, + }, + ], + false, + true, + ); }) .catch((error) => { Log.warn('Error downloading test receipt:', {message: error}); @@ -551,6 +582,10 @@ function IOURequestStepScan({ }, ); + const dismissMultiScanEducationalPopup = () => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + }; + /** * Sets the Receipt objects and navigates the user to the next page */ @@ -800,22 +835,23 @@ function IOURequestStepScan({ )} - {}} - onClose={() => {}} - titleStyles={styles.mb2} - confirmText={translate('common.buttonConfirm')} - description={translate('iou.scanMultipleReceiptsDescription')} - shouldGoBack={false} - /> + {shouldShowMultiScanEducationalPopup && ( + + )} setIsLoaderVisible(true)}> {({openPicker}) => ( From 720dc4f6f0acc57940e065283d99b8b7aaecefad Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 4 Jun 2025 16:22:05 +0200 Subject: [PATCH 05/11] feat: open modal on toggling multiscan --- src/components/FeatureTrainingModal.tsx | 2 +- .../request/step/IOURequestStepScan/index.native.tsx | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index eebb1f595abc3..8187b6b11f19e 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -65,7 +65,7 @@ type BaseFeatureTrainingModalProps = { secondaryDescription?: string; /** Style for the title */ - titleStyles: StyleProp; + titleStyles?: StyleProp; /** Whether to show `Don't show me this again` option */ shouldShowDismissModalOption?: boolean; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 68543e5c4d0dc..0241ab47d464d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -128,8 +128,7 @@ function IOURequestStepScan({ const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); const isTabActive = useIsFocused(); - - const shouldShowMultiScanEducationalPopup = !dismissedProductTraining?.[CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL]; + const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const [pdfFile, setPdfFile] = useState(null); @@ -566,7 +565,10 @@ function IOURequestStepScan({ }; const dismissMultiScanEducationalPopup = () => { - dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + InteractionManager.runAfterInteractions(() => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + setShouldShowMultiScanEducationalPopup(false); + }); }; /** @@ -758,6 +760,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); From da6300c6176e99f6eb05995f6b49e14eb5143e4f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 4 Jun 2025 16:42:35 +0200 Subject: [PATCH 06/11] fix: prettier --- src/CONST.ts | 2 +- src/components/FeatureTrainingModal.tsx | 2 +- .../ProductTrainingContext/TOOLTIPS.ts | 5 +-- .../step/IOURequestStepScan/index.native.tsx | 1 - .../request/step/IOURequestStepScan/index.tsx | 34 +++++++++++++++++-- src/styles/index.ts | 2 +- src/types/onyx/DismissedProductTraining.ts | 2 +- 7 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 561c28ee7c8b3..e78698ed55367 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -7114,7 +7114,7 @@ const CONST = { ACCOUNT_SWITCHER: 'accountSwitcher', EXPENSE_REPORTS_FILTER: 'expenseReportsFilter', SCAN_TEST_DRIVE_CONFIRMATION: 'scanTestDriveConfirmation', - MULTI_SCAN_EDUCATIONAL_MODAL: 'multiScanEducationalModal' + 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 8187b6b11f19e..37258daa8b621 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, TextStyle} 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'; diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts index 74f6a0e890939..765047830f952 100644 --- a/src/components/ProductTrainingContext/TOOLTIPS.ts +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -20,10 +20,7 @@ const { MULTI_SCAN_EDUCATIONAL_MODAL, } = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; -type ProductTrainingTooltipName = Exclude< - ValueOf, - typeof MULTI_SCAN_EDUCATIONAL_MODAL ->; +type ProductTrainingTooltipName = Exclude, typeof MULTI_SCAN_EDUCATIONAL_MODAL>; type ShouldShowConditionProps = { shouldUseNarrowLayout: boolean; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 0241ab47d464d..325872deddd8d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -56,7 +56,6 @@ 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 colors from '@styles/theme/colors'; import variables from '@styles/variables'; import { getMoneyRequestParticipantsFromReport, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 639dd8948e5bc..09d79982a954d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -2,7 +2,7 @@ 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'; @@ -10,6 +10,7 @@ import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-nati import type Webcam from 'react-webcam'; import TestReceipt from '@assets/images/fake-receipt.png'; import Hand from '@assets/images/hand.svg'; +import MultiScanHand from '@assets/images/multi-scan-hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; @@ -20,6 +21,7 @@ import CopyTextToClipboard from '@components/CopyTextToClipboard'; import DownloadAppBanner from '@components/DownloadAppBanner'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZoneUI'; +import FeatureTrainingModal from '@components/FeatureTrainingModal'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -127,6 +129,7 @@ function IOURequestStepScan({ const trackRef = useRef(null); const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false); const [elementTop, setElementTop] = useState(0); + const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const getScreenshotTimeoutRef = useRef(null); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); @@ -135,6 +138,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 @@ -775,6 +779,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); @@ -852,6 +859,13 @@ function IOURequestStepScan({ return translate(attachmentInvalidReason); }; + const dismissMultiScanEducationalPopup = () => { + InteractionManager.runAfterInteractions(() => { + dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MULTI_SCAN_EDUCATIONAL_MODAL); + setShouldShowMultiScanEducationalPopup(false); + }); + }; + const mobileCameraView = () => ( <> @@ -998,7 +1012,23 @@ function IOURequestStepScan({ )} - + {canUseMultiScan && isMobile() && shouldShowMultiScanEducationalPopup && ( + + )} backgroundColor: colors.pink700, overflow: 'hidden', height: 220, - } + }, }) satisfies Styles; type ThemeStyles = ReturnType; diff --git a/src/types/onyx/DismissedProductTraining.ts b/src/types/onyx/DismissedProductTraining.ts index 2a9b2b551bc74..e00c81e805c83 100644 --- a/src/types/onyx/DismissedProductTraining.ts +++ b/src/types/onyx/DismissedProductTraining.ts @@ -106,7 +106,7 @@ type DismissedProductTraining = { /** * When user dismisses the MultiScan product training tooltip, we store the timestamp here. */ - [MULTI_SCAN_EDUCATIONAL_MODAL]: DismissedProductTrainingElement + [MULTI_SCAN_EDUCATIONAL_MODAL]: DismissedProductTrainingElement; /** * When user dismisses the ChangeReportPolicy feature training modal, we store the timestamp here. From 909fc46b4753c79c5a894a11ddcc3e52f985d74d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 6 Jun 2025 08:26:43 +0200 Subject: [PATCH 07/11] fix: lint --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 298774ebf4e83..ec7241d4914e2 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -121,7 +121,6 @@ function IOURequestStepScan({ const cameraRef = useRef(null); const trackRef = useRef(null); const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false); - const [elementTop, setElementTop] = useState(0); const [shouldShowMultiScanEducationalPopup, setShouldShowMultiScanEducationalPopup] = useState(false); const getScreenshotTimeoutRef = useRef(null); From 7d0909780b7b41db5d4cd975d8ff018d2be9cc46 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 9 Jun 2025 18:26:43 +0200 Subject: [PATCH 08/11] feat: replace the image --- assets/images/multi-scan-hand.svg | 242 ------------------ assets/images/multi-scan.svg | 126 +++++++++ .../step/IOURequestStepScan/index.native.tsx | 6 +- .../request/step/IOURequestStepScan/index.tsx | 6 +- 4 files changed, 130 insertions(+), 250 deletions(-) delete mode 100644 assets/images/multi-scan-hand.svg create mode 100644 assets/images/multi-scan.svg diff --git a/assets/images/multi-scan-hand.svg b/assets/images/multi-scan-hand.svg deleted file mode 100644 index 0ae6365b9cefe..0000000000000 --- a/assets/images/multi-scan-hand.svg +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/images/multi-scan.svg b/assets/images/multi-scan.svg new file mode 100644 index 0000000000000..18d775d35171a --- /dev/null +++ b/assets/images/multi-scan.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 4c0dbdfbacf96..ffa1ca39a34b0 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -14,7 +14,7 @@ import {useCameraDevice} from 'react-native-vision-camera'; import type {TupleToUnion} from 'type-fest'; import TestReceipt from '@assets/images/fake-receipt.png'; import Hand from '@assets/images/hand.svg'; -import MultiScanHand from '@assets/images/multi-scan-hand.svg'; +import MultiScan from '@assets/images/multi-scan.svg'; import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; @@ -866,13 +866,11 @@ function IOURequestStepScan({ {shouldShowMultiScanEducationalPopup && ( Date: Tue, 10 Jun 2025 10:54:51 +0200 Subject: [PATCH 09/11] fix: styles for the image --- .../educational-illustration__multi-scan.svg | 380 ++++++++++++++++++ assets/images/multi-scan.svg | 126 ------ src/components/FeatureTrainingModal.tsx | 6 +- .../step/IOURequestStepScan/index.native.tsx | 4 +- .../request/step/IOURequestStepScan/index.tsx | 5 +- src/styles/index.ts | 3 +- 6 files changed, 391 insertions(+), 133 deletions(-) create mode 100644 assets/images/educational-illustration__multi-scan.svg delete mode 100644 assets/images/multi-scan.svg 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/assets/images/multi-scan.svg b/assets/images/multi-scan.svg deleted file mode 100644 index 18d775d35171a..0000000000000 --- a/assets/images/multi-scan.svg +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 4ed4a29b6fee7..17a7532b4e3ec 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -5,6 +5,7 @@ import {Image, InteractionManager, View} 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 ImageSVGProps from '@components/ImageSVG/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -147,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 @@ -370,6 +371,7 @@ function FeatureTrainingModal({ } : {}), ...modalInnerContainerStyle, + padding: 0, }} > multiScanEducationalPopupImage: { backgroundColor: colors.pink700, overflow: 'hidden', - height: 220, + paddingHorizontal: 0, + aspectRatio: 1.7, }, }) satisfies Styles; From b334d11032955ce729227f7e7ccc47c38815a5f8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 10 Jun 2025 11:13:13 +0200 Subject: [PATCH 10/11] fix: eslint and prettier --- src/components/FeatureTrainingModal.tsx | 2 +- src/pages/iou/request/step/IOURequestStepScan/index.native.tsx | 2 +- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 17a7532b4e3ec..4d59135a14989 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -5,7 +5,6 @@ import {Image, InteractionManager, View} 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 ImageSVGProps from '@components/ImageSVG/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -21,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'; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 5f882eff41a2a..f4ed982554c71 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -12,9 +12,9 @@ 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 MultiScan from '@assets/images/educational-illustration__multi-scan.svg'; import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 9a0cce06086cf..a8173c07375c3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -8,9 +8,9 @@ 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 MultiScan from '@assets/images/educational-illustration__multi-scan.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; import type {FileObject} from '@components/AttachmentModal'; From 1d903ac19b075ed92c89ce21fbafcb3f442417c1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 10 Jun 2025 12:54:51 +0200 Subject: [PATCH 11/11] fix: revert unnecessary change --- src/components/FeatureTrainingModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 4d59135a14989..372b8c80f2995 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -371,7 +371,6 @@ function FeatureTrainingModal({ } : {}), ...modalInnerContainerStyle, - padding: 0, }} >