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;