diff --git a/assets/images/simple-illustrations/simple-illustration__messageinabottle.svg b/assets/images/simple-illustrations/simple-illustration__messageinabottle.svg new file mode 100644 index 000000000000..351995839832 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__messageinabottle.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__replace-receipt.svg b/assets/images/simple-illustrations/simple-illustration__replace-receipt.svg new file mode 100644 index 000000000000..2c75fd5f110e --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__replace-receipt.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__smartscan.svg b/assets/images/simple-illustrations/simple-illustration__smartscan.svg new file mode 100644 index 000000000000..956ebea7320f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__smartscan.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 448af0b44569..742b3fd7546d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -814,6 +814,7 @@ const CONST = { GLOBAL_REIMBURSEMENTS_ON_ND: 'globalReimbursementsOnND', PRIVATE_DOMAIN_ONBOARDING: 'privateDomainOnboarding', IS_TRAVEL_VERIFIED: 'isTravelVerified', + NEWDOT_MULTI_FILES_DRAG_AND_DROP: 'newDotMultiFilesDragAndDrop', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/DropZoneUI.tsx b/src/components/DropZoneUI.tsx new file mode 100644 index 000000000000..e20405499793 --- /dev/null +++ b/src/components/DropZoneUI.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; +import DragAndDropConsumer from './DragAndDrop/Consumer'; +import Icon from './Icon'; +import Text from './Text'; + +type DropZoneUIProps = { + /** Callback to execute when a file is dropped. */ + onDrop: (event: DragEvent) => void; + + /** Icon to display in the drop zone */ + icon: IconAsset; + + /** Title to display in the drop zone */ + dropTitle?: string; + + /** Custom styles for the drop zone */ + dropStyles?: StyleProp; + + /** Custom styles for the drop zone text */ + dropTextStyles?: StyleProp; + + /** Custom styles for the inner wrapper of the drop zone */ + dropInnerWrapperStyles?: StyleProp; +}; + +function DropZoneUI({onDrop, icon, dropTitle, dropStyles, dropTextStyles, dropInnerWrapperStyles}: DropZoneUIProps) { + const styles = useThemeStyles(); + + return ( + + + + {/* TODO: display dropInnerWrapper styles only when hovered over - will be done in Stage 4 (two zones) */} + + + + + {dropTitle} + + + + + ); +} + +DropZoneUI.displayName = 'DropZoneUI'; + +export default DropZoneUI; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 28e133620756..dda9a9f7ff7f 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -182,6 +182,9 @@ import Shield from '@assets/images/shield.svg'; import AppleLogo from '@assets/images/signIn/apple-logo.svg'; import GoogleLogo from '@assets/images/signIn/google-logo.svg'; import AdvancedApprovalsSquare from '@assets/images/simple-illustrations/advanced-approvals-icon-square.svg'; +import MessageInABottle from '@assets/images/simple-illustrations/simple-illustration__messageinabottle.svg'; +import ReplaceReceipt from '@assets/images/simple-illustrations/simple-illustration__replace-receipt.svg'; +import SmartScan from '@assets/images/simple-illustrations/simple-illustration__smartscan.svg'; import Facebook from '@assets/images/social-facebook.svg'; import Instagram from '@assets/images/social-instagram.svg'; import Linkedin from '@assets/images/social-linkedin.svg'; @@ -338,6 +341,7 @@ export { Menu, Meter, Megaphone, + MessageInABottle, MoneyBag, MoneyCircle, MoneySearch, @@ -371,11 +375,13 @@ export { ReceiptSlash, RemoveMembers, ReceiptSearch, + ReplaceReceipt, Rotate, RotateLeft, Scan, Send, Shield, + SmartScan, Stopwatch, Suitcase, Sync, diff --git a/src/languages/en.ts b/src/languages/en.ts index b5adf3db2d2e..96b83371b478 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -595,6 +595,11 @@ const translations = { tooManyFiles: ({fileLimit}: FileLimitParams) => `You can only upload up to ${fileLimit} files at a time.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB} MB. Please try again.`, }, + dropzone: { + addAttachments: 'Add attachments', + scanReceipts: 'Scan receipts', + replaceReceipt: 'Replace receipt', + }, filePicker: { fileError: 'File error', errorWhileSelectingFile: 'An error occurred while selecting an file. Please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index dc4cdc9fa59a..c8773708727c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -590,6 +590,11 @@ const translations = { tooManyFiles: ({fileLimit}: FileLimitParams) => `Solamente puedes suber ${fileLimit} archivos a la vez.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB} MB. Por favor, vuelve a intentarlo.`, }, + dropzone: { + addAttachments: 'AƱadir archivos adjuntos', + scanReceipts: 'Escanear recibos', + replaceReceipt: 'Reemplazar recibo', + }, filePicker: { fileError: 'Error de archivo', errorWhileSelectingFile: 'An error occurred while selecting an file. Please try again.', diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index d71a92b58868..d8cf622d3eec 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -74,6 +74,10 @@ function canUseCallScheduling() { return false; } +function canUseMultiFilesDragAndDrop(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) || canUseAllBetas(betas); +} + export default { canUseDefaultRooms, canUseLinkPreviews, @@ -91,4 +95,5 @@ export default { canUseGlobalReimbursementsOnND, canUsePrivateDomainOnboarding, canUseCallScheduling, + canUseMultiFilesDragAndDrop, }; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 336e2a7c5b2c..91c34d0d063a 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -9,8 +9,10 @@ import {runOnUI, useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentModal from '@components/AttachmentModal'; +import DropZoneUI from '@components/DropZoneUI'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; +import * as Expensicons from '@components/Icon/Expensicons'; import ImportedStateIndicator from '@components/ImportedStateIndicator'; import type {Mention} from '@components/MentionSuggestions'; import OfflineIndicator from '@components/OfflineIndicator'; @@ -22,6 +24,7 @@ import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLen import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitleLength'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -116,8 +119,11 @@ function ReportActionCompose({ const actionButtonRef = useRef(null); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const personalDetails = usePersonalDetails(); - const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE); - const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT); + const [blockedFromConcierge] = useOnyx(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, {canBeMissing: true}); + const [shouldShowComposeInput = true] = useOnyx(ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, {canBeMissing: true}); + + // TODO: remove canUseMultiFilesDragAndDrop check after the feature is enabled + const {canUseMultiFilesDragAndDrop} = usePermissions(); /** * Updates the Highlight state of the composer @@ -480,18 +486,39 @@ function ReportActionCompose({ onValueChange={onValueChange} didHideComposerInput={didHideComposerInput} /> - { - if (isAttachmentPreviewActive) { - return; - } - const data = event.dataTransfer?.files[0]; - if (data) { - data.uri = URL.createObjectURL(data); - displayFileInModal(data); - } - }} - /> + {/* TODO: remove canUseMultiFilesDragAndDrop check after the feature is enabled */} + {canUseMultiFilesDragAndDrop ? ( + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.files[0]; + if (data) { + data.uri = URL.createObjectURL(data); + displayFileInModal(data); + } + }} + icon={Expensicons.MessageInABottle} + dropTitle={translate('dropzone.addAttachments')} + dropStyles={styles.attachmentDropOverlay} + dropTextStyles={styles.attachmentDropText} + dropInnerWrapperStyles={styles.attachmentDropInnerWrapper} + /> + ) : ( + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.files[0]; + if (data) { + data.uri = URL.createObjectURL(data); + displayFileInModal(data); + } + }} + /> + )} )} diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index a1c94aac3007..bec60ea04645 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -8,6 +8,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -119,6 +120,8 @@ function IOURequestStartPage({ const shouldShowPerDiemOption = iouType !== CONST.IOU.TYPE.SPLIT && iouType !== CONST.IOU.TYPE.TRACK && ((!isFromGlobalCreate && doesCurrentPolicyPerDiemExist) || (isFromGlobalCreate && doesPerDiemPolicyExist)); + const {canUseMultiFilesDragAndDrop} = usePermissions(); + return ( diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 99d7d5d895cb..2eb38ea030ea 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -6,6 +6,7 @@ import {useOnyx} from 'react-native-onyx'; import type {FileObject} from '@components/AttachmentModal'; import ConfirmModal from '@components/ConfirmModal'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; +import DropZoneUI from '@components/DropZoneUI'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -18,6 +19,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useFetchRoute from '@hooks/useFetchRoute'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import useThreeDotsAnchorPosition from '@hooks/useThreeDotsAnchorPosition'; import DateUtils from '@libs/DateUtils'; @@ -154,6 +156,9 @@ function IOURequestStepConfirmation({ const [isConfirmed, setIsConfirmed] = useState(false); const [isConfirming, setIsConfirming] = useState(false); + // TODO: remove canUseMultiFilesDragAndDrop check after the feature is enabled + const {canUseMultiFilesDragAndDrop} = usePermissions(); + const headerTitle = useMemo(() => { if (isCategorizingTrackExpense) { return translate('iou.categorize'); @@ -891,7 +896,7 @@ function IOURequestStepConfirmation({ {(isLoading || isLoadingReceipt) && } {PDFThumbnailView} - { - const file = e?.dataTransfer?.files[0]; - if (file) { - file.uri = URL.createObjectURL(file); - setReceiptOnDrop(file); - } - }} - /> + {/* TODO: remove canUseMultiFilesDragAndDrop check after the feature is enabled */} + {canUseMultiFilesDragAndDrop ? ( + { + const file = e?.dataTransfer?.files[0]; + if (file) { + file.uri = URL.createObjectURL(file); + setReceiptOnDrop(file); + } + }} + icon={Expensicons.ReplaceReceipt} + dropStyles={styles.receiptDropOverlay} + dropTitle={translate('dropzone.replaceReceipt')} + dropTextStyles={styles.receiptDropText} + dropInnerWrapperStyles={styles.receiptDropInnerWrapper} + /> + ) : ( + { + const file = e?.dataTransfer?.files[0]; + if (file) { + file.uri = URL.createObjectURL(file); + setReceiptOnDrop(file); + } + }} + /> + )} { + setMoneyRequestParticipants( + transactionID, + [ + { + ...managerMcTestParticipant, + reportID: reportIDParam, + selected: true, + }, + ], + true, + ).then(() => { navigateToConfirmationPage(true, reportIDParam); }); return; @@ -773,7 +788,10 @@ function IOURequestStepScan({ setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + style={{ + ...styles.videoContainer, + display: cameraPermissionState !== 'granted' ? 'none' : 'block', + }} ref={cameraRef} screenshotFormat="image/png" videoConstraints={videoConstraints} @@ -922,16 +940,34 @@ function IOURequestStepScan({ {!(isDraggingOver ?? isDraggingOverWrapper) && (isMobile() ? mobileCameraView() : desktopUploadView())} - { - const file = e?.dataTransfer?.files[0]; - if (file) { - file.uri = URL.createObjectURL(file); - setReceiptAndNavigate(file); - } - }} - receiptImageTopPosition={receiptImageTopPosition} - /> + {/* TODO: remove canUseMultiFilesDragAndDrop check after the feature is enabled */} + {canUseMultiFilesDragAndDrop ? ( + { + const file = e?.dataTransfer?.files[0]; + if (file) { + file.uri = URL.createObjectURL(file); + setReceiptAndNavigate(file); + } + }} + icon={Expensicons.SmartScan} + dropStyles={styles.receiptDropOverlay} + dropTitle={translate('dropzone.scanReceipts')} + dropTextStyles={styles.receiptDropText} + dropInnerWrapperStyles={styles.receiptDropInnerWrapper} + /> + ) : ( + { + const file = e?.dataTransfer?.files[0]; + if (file) { + file.uri = URL.createObjectURL(file); + setReceiptAndNavigate(file); + } + }} + receiptImageTopPosition={receiptImageTopPosition} + /> + )} {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/styles/index.ts b/src/styles/index.ts index 7845adbb3493..205c8ffc5632 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1546,6 +1546,12 @@ const styles = (theme: ThemeColors) => textAlign: 'center', }, + textDropZone: { + ...headlineFont, + fontSize: variables.fontSizeXLarge, + textAlign: 'center', + }, + subTextFileUpload: { ...FontUtils.fontFamily.platform.EXP_NEUE, lineHeight: variables.lineHeightLarge, @@ -4006,6 +4012,17 @@ const styles = (theme: ThemeColors) => height: 200, }, + dropWrapper: { + zIndex: 2, + backgroundColor: theme.dropWrapperBG, + }, + + dropInnerWrapper: { + borderWidth: 2, + flex: 1, + borderStyle: 'dashed', + }, + reportDropOverlay: { backgroundColor: theme.dropUIBG, zIndex: 2, @@ -4013,9 +4030,33 @@ const styles = (theme: ThemeColors) => fileDropOverlay: { backgroundColor: theme.fileDropUIBG, + }, + + attachmentDropOverlay: { + backgroundColor: theme.attachmentDropUIBG, + }, + + attachmentDropText: { + color: theme.textAttachmentDropZone, + }, + + attachmentDropInnerWrapper: { + borderColor: theme.attachmentDropBorderColor, + }, + + receiptDropOverlay: { + backgroundColor: theme.receiptDropUIBG, zIndex: 2, }, + receiptDropText: { + color: theme.textReceiptDropZone, + }, + + receiptDropInnerWrapper: { + borderColor: theme.receiptDropBorderColor, + }, + isDraggingOver: { backgroundColor: theme.fileDropUIBG, }, diff --git a/src/styles/theme/themes/dark.ts b/src/styles/theme/themes/dark.ts index 3ecd88e1277d..c6d0240662eb 100644 --- a/src/styles/theme/themes/dark.ts +++ b/src/styles/theme/themes/dark.ts @@ -22,6 +22,8 @@ const darkTheme = { textSupporting: colors.productDark800, text: colors.productDark900, textColorfulBackground: colors.ivory, + textReceiptDropZone: colors.green700, + textAttachmentDropZone: colors.blue700, syntax: colors.productDark800, link: colors.blue300, linkHover: colors.blue100, @@ -68,7 +70,12 @@ const darkTheme = { heroCard: colors.blue400, uploadPreviewActivityIndicator: colors.productDark200, dropUIBG: 'rgba(6,27,9,0.92)', + dropWrapperBG: 'rgba(26, 61, 50, 0.72)', fileDropUIBG: 'rgba(3, 212, 124, 0.84)', + attachmentDropUIBG: 'rgba(90, 176, 255, 0.9)', + attachmentDropBorderColor: colors.blue100, + receiptDropUIBG: 'rgba(3, 212, 124, 0.9)', + receiptDropBorderColor: colors.green100, checkBox: colors.green400, imageCropBackgroundColor: colors.productDark700, fallbackIconColor: colors.green700, diff --git a/src/styles/theme/themes/light.ts b/src/styles/theme/themes/light.ts index 2c17f43eaa9e..136c80f525be 100644 --- a/src/styles/theme/themes/light.ts +++ b/src/styles/theme/themes/light.ts @@ -22,6 +22,8 @@ const lightTheme = { textSupporting: colors.productLight800, text: colors.productLight900, textColorfulBackground: colors.ivory, + textReceiptDropZone: colors.green700, + textAttachmentDropZone: colors.blue700, syntax: colors.productLight800, link: colors.blue600, linkHover: colors.blue500, @@ -68,7 +70,12 @@ const lightTheme = { heroCard: colors.blue400, uploadPreviewActivityIndicator: colors.productLight200, dropUIBG: 'rgba(252, 251, 249, 0.92)', + dropWrapperBG: 'rgba(235, 230, 223, 0.72)', fileDropUIBG: 'rgba(3, 212, 124, 0.84)', + attachmentDropUIBG: 'rgba(90, 176, 255, 0.9)', + attachmentDropBorderColor: colors.blue100, + receiptDropUIBG: 'rgba(3, 212, 124, 0.9)', + receiptDropBorderColor: colors.green100, checkBox: colors.green400, imageCropBackgroundColor: colors.productLight700, fallbackIconColor: colors.green700, diff --git a/src/styles/theme/types.ts b/src/styles/theme/types.ts index a7bc07d330e5..c0061ce7a198 100644 --- a/src/styles/theme/types.ts +++ b/src/styles/theme/types.ts @@ -28,6 +28,8 @@ type ThemeColors = { textSupporting: Color; text: Color; textColorfulBackground: Color; + textReceiptDropZone: Color; + textAttachmentDropZone: Color; syntax: Color; link: Color; linkHover: Color; @@ -73,7 +75,12 @@ type ThemeColors = { heroCard: Color; uploadPreviewActivityIndicator: Color; dropUIBG: Color; + dropWrapperBG: Color; fileDropUIBG: Color; + attachmentDropUIBG: Color; + attachmentDropBorderColor: Color; + receiptDropUIBG: Color; + receiptDropBorderColor: Color; checkBox: Color; imageCropBackgroundColor: Color; fallbackIconColor: Color;