diff --git a/assets/images/sparkles.svg b/assets/images/sparkles.svg
new file mode 100644
index 0000000000000..d878a4e49f1b1
--- /dev/null
+++ b/assets/images/sparkles.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 9f57a60e79b9e..a982afbc75eaf 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -8137,6 +8137,7 @@ const CONST = {
},
SELECTION_LIST_WITH_SECTIONS: {
BASE_LIST_ITEM: 'SelectionListWithSections-BaseListItem',
+ SELECT_ALL: 'SelectionListWithSections-SelectAll',
},
CONTEXT_MENU: {
REPLY_IN_THREAD: 'ContextMenu-ReplyInThread',
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 60746b906dfc4..1468f94525e50 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -138,6 +138,7 @@ import Instagram from '@assets/images/social-instagram.svg';
import Linkedin from '@assets/images/social-linkedin.svg';
import Podcast from '@assets/images/social-podcast.svg';
import Twitter from '@assets/images/social-twitter.svg';
+import Sparkles from '@assets/images/sparkles.svg';
import SpreadsheetComputer from '@assets/images/spreadsheet-computer.svg';
import Star from '@assets/images/Star.svg';
import Stopwatch from '@assets/images/stopwatch.svg';
@@ -266,6 +267,7 @@ export {
Scan,
Send,
Shield,
+ Sparkles,
Stopwatch,
Sync,
Task,
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 8255f17c6f249..c846f1550ba9a 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -204,6 +204,7 @@ import Linkedin from '@assets/images/social-linkedin.svg';
import Podcast from '@assets/images/social-podcast.svg';
import Twitter from '@assets/images/social-twitter.svg';
import Youtube from '@assets/images/social-youtube.svg';
+import Sparkles from '@assets/images/sparkles.svg';
import SpreadsheetComputer from '@assets/images/spreadsheet-computer.svg';
import Star from '@assets/images/Star.svg';
import Stopwatch from '@assets/images/stopwatch.svg';
@@ -463,6 +464,7 @@ const Expensicons = {
Feed,
Table,
SpreadsheetComputer,
+ Sparkles,
Bookmark,
Star,
QBDSquare,
diff --git a/src/components/ImageWithLoading.tsx b/src/components/ImageWithLoading.tsx
index 513e113fd75f4..3a84db339adf8 100644
--- a/src/components/ImageWithLoading.tsx
+++ b/src/components/ImageWithLoading.tsx
@@ -87,6 +87,7 @@ function ImageWithLoading({
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
style={[styles.w100, styles.h100, style]}
+ resizeMode={resizeMode}
onLoadStart={() => {
if (isLoadedRef.current ?? isLoading) {
return;
diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx
index 0488d3a541815..afde73cf29a51 100644
--- a/src/components/ImageWithSizeCalculation.tsx
+++ b/src/components/ImageWithSizeCalculation.tsx
@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
-import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
+import type {ImageResizeMode, ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import CONST from '@src/CONST';
@@ -46,6 +46,9 @@ type ImageWithSizeCalculationProps = {
/** Callback to be called when the image loads */
onLoad?: (event: {nativeEvent: {width: number; height: number}}) => void;
+
+ /** The resize mode of the image */
+ resizeMode?: ImageResizeMode;
};
/**
@@ -65,6 +68,7 @@ function ImageWithSizeCalculation({
loadingIconSize,
loadingIndicatorStyles,
onLoad,
+ resizeMode,
}: ImageWithSizeCalculationProps) {
const styles = useThemeStyles();
@@ -82,7 +86,7 @@ function ImageWithSizeCalculation({
source={source}
aria-label={altText}
isAuthTokenRequired={isAuthTokenRequired}
- resizeMode={RESIZE_MODES.cover}
+ resizeMode={resizeMode ?? RESIZE_MODES.cover}
onError={onError}
onLoad={(event: OnLoadNativeEvent) => {
onMeasure({
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index e8f2f699be65a..1af9e959b822f 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -211,6 +211,9 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Label to be displayed on the right */
rightLabel?: string;
+ /** Icon to be displayed next to the right label */
+ rightLabelIcon?: IconAsset;
+
/** Text to display for the item */
title?: string;
@@ -498,6 +501,7 @@ function MenuItem({
titleContainerStyle,
subtitle,
shouldShowBasicTitle,
+ rightLabelIcon,
label,
shouldTruncateTitle = false,
characterLimit = 200,
@@ -1018,7 +1022,15 @@ function MenuItem({
)}
{!title && !!rightLabel && !errorText && (
-
+
+ {!!rightLabelIcon && (
+
+ )}
{rightLabel}
)}
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 053e456ef7072..33df45196033e 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -75,6 +75,7 @@ import type {DropdownOption} from './ButtonWithDropdownMenu/types';
import {DelegateNoAccessContext} from './DelegateNoAccessModalProvider';
import FormHelpMessage from './FormHelpMessage';
import MoneyRequestAmountInput from './MoneyRequestAmountInput';
+import getContentContainerStyle from './MoneyRequestConfirmationList/getContentContainerStyles';
import MoneyRequestConfirmationListFooter from './MoneyRequestConfirmationListFooter';
import {PressableWithFeedback} from './Pressable';
import {useProductTrainingContext} from './ProductTrainingContext';
@@ -417,6 +418,7 @@ function MoneyRequestConfirmationList({
const [didConfirm, setDidConfirm] = useState(isConfirmed);
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
+ const [showMoreFields, setShowMoreFields] = useState(false);
// Clear the form error if it's set to one among the list passed as an argument
const clearFormErrors = useCallback(
@@ -1239,6 +1241,8 @@ function MoneyRequestConfirmationList({
reportID,
]);
+ const isCompactMode = useMemo(() => !showMoreFields && isScanRequest, [isScanRequest, showMoreFields]);
+
const listFooterContent = (
);
@@ -1315,6 +1321,8 @@ function MoneyRequestConfirmationList({
containerStyle={[styles.flexBasisAuto]}
removeClippedSubviews={false}
disableKeyboardShortcuts
+ contentContainerStyle={getContentContainerStyle(isCompactMode, styles.flex1).contentContainerStyle}
+ ListFooterComponentStyle={isCompactMode ? [styles.flex1] : undefined}
/>
);
diff --git a/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.native.ts b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.native.ts
new file mode 100644
index 0000000000000..7531fdc97b63a
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.native.ts
@@ -0,0 +1,7 @@
+import type GetContentContainerStyle from './types';
+
+const getContentContainerStyle: GetContentContainerStyle = () => ({
+ contentContainerStyle: undefined,
+});
+
+export default getContentContainerStyle;
diff --git a/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.ts b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.ts
new file mode 100644
index 0000000000000..79e704c2f9fbf
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/index.ts
@@ -0,0 +1,7 @@
+import type GetContentContainerStyle from './types';
+
+const getContentContainerStyle: GetContentContainerStyle = (isCompactMode, flex1Style) => ({
+ contentContainerStyle: isCompactMode ? [flex1Style] : undefined,
+});
+
+export default getContentContainerStyle;
diff --git a/src/components/MoneyRequestConfirmationList/getContentContainerStyles/types.ts b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/types.ts
new file mode 100644
index 0000000000000..52a4c05e9378d
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationList/getContentContainerStyles/types.ts
@@ -0,0 +1,10 @@
+import type {StyleProp, ViewStyle} from 'react-native';
+
+type GetContentContainerStyle = (
+ isCompactMode: boolean,
+ flex1Style: ViewStyle,
+) => {
+ contentContainerStyle: StyleProp | undefined;
+};
+
+export default GetContentContainerStyle;
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index a84ee387b2926..5700da4ca8fa3 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -2,8 +2,9 @@ import {emailSelector} from '@selectors/Session';
import {format} from 'date-fns';
import {Str} from 'expensify-common';
import {deepEqual} from 'fast-equals';
-import React, {memo, useMemo} from 'react';
+import React, {memo, useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
+import type {ImageResizeMode, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import useCurrencyList from '@hooks/useCurrencyList';
@@ -15,7 +16,9 @@ import useOnyx from '@hooks/useOnyx';
import useOutstandingReports from '@hooks/useOutstandingReports';
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
import usePrevious from '@hooks/usePrevious';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import {getDecodedCategoryName} from '@libs/CategoryUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
@@ -36,10 +39,13 @@ import {
isCreatedMissing,
isFetchingWaypointsFromServer,
isManagedCardTransaction,
+ isScanRequest,
shouldShowAttendees as shouldShowAttendeesTransactionUtils,
+ willFieldBeAutomaticallyFilled,
} from '@libs/TransactionUtils';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {IOUAction, IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -49,15 +55,19 @@ import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Unit} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Badge from './Badge';
+import Button from './Button';
import ConfirmedRoute from './ConfirmedRoute';
import MentionReportContext from './HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
+import Icon from './Icon';
import MenuItem from './MenuItem';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
+import getImageCompactModeStyle from './MoneyRequestConfirmationListFooter/getImageCompactModeStyle';
import PDFThumbnail from './PDFThumbnail';
import PressableWithoutFocus from './Pressable/PressableWithoutFocus';
import ReceiptEmptyState from './ReceiptEmptyState';
import ReceiptImage from './ReceiptImage';
import {ShowContextMenuContext} from './ShowContextMenuContext';
+import Text from './Text';
type MoneyRequestConfirmationListFooterProps = {
/** The action to perform */
@@ -227,6 +237,12 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if the description is required */
isDescriptionRequired: boolean;
+
+ /** Whether to show more fields */
+ showMoreFields: boolean;
+
+ /** Function to set the show more fields */
+ setShowMoreFields: (showMoreFields: boolean) => void;
};
function MoneyRequestConfirmationListFooter({
@@ -286,13 +302,15 @@ function MoneyRequestConfirmationListFooter({
onToggleReimbursable,
isReceiptEditable = false,
isDescriptionRequired = false,
+ showMoreFields,
+ setShowMoreFields,
}: MoneyRequestConfirmationListFooterProps) {
- const icons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'CalendarSolid']);
+ const icons = useMemoizedLazyExpensifyIcons(['Stopwatch', 'CalendarSolid', 'Sparkles', 'DownArrow'] as const);
const styles = useThemeStyles();
+ const theme = useTheme();
const {translate, toLocaleDigit, localeCompare} = useLocalize();
const {getCurrencySymbol} = useCurrencyList();
const {isOffline} = useNetwork();
-
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true});
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true});
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true});
@@ -307,6 +325,7 @@ function MoneyRequestConfirmationListFooter({
const isCreatingTrackExpense = action === CONST.IOU.ACTION.CREATE && iouType === CONST.IOU.TYPE.TRACK;
const decodedCategoryName = useMemo(() => getDecodedCategoryName(iouCategory), [iouCategory]);
+ const isScan = isScanRequest(transaction);
const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
const shouldShowTags = useMemo(
@@ -418,6 +437,7 @@ function MoneyRequestConfirmationListFooter({
} = receiptPath && receiptFilename ? getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : ({} as ThumbnailAndImageURI);
const resolvedThumbnail = isLocalFile ? receiptThumbnail : tryResolveUrlFromApiRoot(receiptThumbnail ?? '');
const resolvedReceiptImage = isLocalFile ? receiptImage : tryResolveUrlFromApiRoot(receiptImage ?? '');
+ const shouldRequireAuthToken = !!receiptThumbnail && !isLocalFile;
const shouldNavigateToUpgradePath = !policyForMovingExpensesID && !shouldSelectPolicy;
// Time requests appear as regular expenses after they're created, with editable amount and merchant, not hours and rate
@@ -452,6 +472,23 @@ function MoneyRequestConfirmationListFooter({
const mentionReportContextValue = useMemo(() => ({currentReportID: reportID, exactlyMatch: true}), [reportID]);
+ const getRightLabelIcon = useCallback(() => {
+ return willFieldBeAutomaticallyFilled(transaction, 'category') ? icons.Sparkles : undefined;
+ }, [transaction, icons.Sparkles]);
+
+ const getRightLabel = useCallback(
+ (isRequiredField = false) => {
+ if (willFieldBeAutomaticallyFilled(transaction, 'category')) {
+ return translate('common.automatic');
+ }
+ if (isRequiredField) {
+ return translate('common.required');
+ }
+ return '';
+ },
+ [transaction, translate],
+ );
+
const fields = [
{
item: (
@@ -478,6 +515,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowSmartScanFields && shouldShowAmountField,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -511,6 +549,7 @@ function MoneyRequestConfirmationListFooter({
),
shouldShow: true,
+ shouldShowAboveShowMore: true,
},
{
item: (
@@ -543,6 +582,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: isDistanceRequest,
+ shouldShowAboveShowMore: true,
},
{
item: (
@@ -580,6 +620,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: isDistanceRequest,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -606,6 +647,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowMerchant,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -687,12 +729,14 @@ function MoneyRequestConfirmationListFooter({
titleStyle={styles.flex1}
disabled={didConfirm}
interactive={!isReadOnly}
- rightLabel={isCategoryRequired ? translate('common.required') : ''}
+ rightLabel={getRightLabel(isCategoryRequired)}
+ rightLabelIcon={getRightLabelIcon()}
brickRoadIndicator={shouldDisplayCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={shouldDisplayCategoryError ? translate(formError) : ''}
/>
),
shouldShow: shouldShowCategories,
+ shouldShowAboveShowMore: isCategoryRequired,
},
{
item: (
@@ -718,6 +762,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowDate,
+ shouldShowAboveShowMore: false,
},
...policyTagLists.map(({name}, index) => {
const tagVisibilityItem = tagVisibility.at(index);
@@ -751,6 +796,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow,
+ shouldShowAboveShowMore: isTagRequired,
};
}),
{
@@ -776,6 +822,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowTax,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -798,6 +845,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowTax,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -824,6 +872,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: shouldShowAttendees,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -843,6 +892,7 @@ function MoneyRequestConfirmationListFooter({
),
shouldShow: shouldShowReimbursable,
isSupplementary: true,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -861,6 +911,7 @@ function MoneyRequestConfirmationListFooter({
),
shouldShow: shouldShowBillable,
+ shouldShowAboveShowMore: false,
},
{
item: (
@@ -882,6 +933,7 @@ function MoneyRequestConfirmationListFooter({
/>
),
shouldShow: isPolicyExpenseChat,
+ shouldShowAboveShowMore: false,
},
];
@@ -944,9 +996,33 @@ function MoneyRequestConfirmationListFooter({
return badges;
}, [firstDay, lastDay, translate, tripDays, icons]);
- const receiptThumbnailContent = useMemo(
- () => (
-
+ const {windowWidth} = useWindowDimensions();
+ const isCompactMode = useMemo(() => !showMoreFields && isScan, [isScan, showMoreFields]);
+ const [receiptAspectRatio, setReceiptAspectRatio] = useState(null);
+
+ const handleReceiptLoad = useCallback((event?: {nativeEvent: {width: number; height: number}}) => {
+ const width = event?.nativeEvent.width ?? 0;
+ const height = event?.nativeEvent.height ?? 0;
+ if (!width || !height) {
+ return;
+ }
+ const ratio = width / height;
+ setReceiptAspectRatio((previousRatio) => (ratio === previousRatio ? previousRatio : ratio));
+ }, []);
+
+ const receiptSizeStyle = styles.expenseViewImageSmall;
+ let receiptHeightStyle: ViewStyle | undefined;
+ let receiptResizeMode: ImageResizeMode | undefined;
+ const horizontalMargin = typeof styles.moneyRequestImage.marginHorizontal === 'number' ? styles.moneyRequestImage.marginHorizontal : 0;
+ if (isCompactMode) {
+ const availableWidth = windowWidth - horizontalMargin * 2;
+ receiptHeightStyle = getImageCompactModeStyle(variables.receiptPreviewMaxWidth, availableWidth, receiptAspectRatio);
+ receiptResizeMode = 'cover';
+ }
+
+ const receiptThumbnailContent = useMemo(() => {
+ return (
+
{isLocalFile && Str.isPDF(receiptFilename) ? (
{
@@ -965,12 +1041,12 @@ function MoneyRequestConfirmationListFooter({
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.PDF_RECEIPT_THUMBNAIL}
disabled={!shouldDisplayReceipt}
disabledStyle={styles.cursorDefault}
- style={styles.h100}
+ style={isCompactMode ? receiptHeightStyle : styles.h100}
>
@@ -993,7 +1069,7 @@ function MoneyRequestConfirmationListFooter({
accessibilityLabel={translate('accessibilityHints.viewAttachment')}
sentryLabel={CONST.SENTRY_LABEL.REQUEST_CONFIRMATION_LIST.RECEIPT_THUMBNAIL}
disabledStyle={styles.cursorDefault}
- style={[styles.h100, styles.flex1]}
+ style={isCompactMode ? [receiptHeightStyle, styles.flex1] : [styles.h100, styles.flex1]}
>
)}
- ),
- [
- styles.moneyRequestImage,
- styles.expenseViewImageSmall,
- styles.cursorDefault,
- styles.h100,
- styles.flex1,
- isLocalFile,
- receiptFilename,
- translate,
- shouldDisplayReceipt,
- resolvedReceiptImage,
- onPDFLoadError,
- onPDFPassword,
- isThumbnail,
- resolvedThumbnail,
- receiptThumbnail,
- fileExtension,
- isDistanceRequest,
- transactionID,
- action,
- iouType,
- reportID,
- isReceiptEditable,
- ],
- );
+ );
+ }, [
+ styles.moneyRequestImage,
+ styles.cursorDefault,
+ styles.h100,
+ styles.flex1,
+ styles.expenseViewImageSmall,
+ receiptSizeStyle,
+ receiptHeightStyle,
+ handleReceiptLoad,
+ isCompactMode,
+ isLocalFile,
+ receiptFilename,
+ translate,
+ shouldDisplayReceipt,
+ resolvedReceiptImage,
+ onPDFLoadError,
+ onPDFPassword,
+ isThumbnail,
+ resolvedThumbnail,
+ fileExtension,
+ isDistanceRequest,
+ transactionID,
+ isReceiptEditable,
+ reportID,
+ action,
+ iouType,
+ shouldRequireAuthToken,
+ ]);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasReceiptImageOrThumbnail = receiptImage || receiptThumbnail;
@@ -1112,7 +1193,7 @@ function MoneyRequestConfirmationListFooter({
>
)}
{(!shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest) && (
-
+
{hasReceiptImageOrThumbnail
? receiptThumbnailContent
: showReceiptEmptyState && (
@@ -1124,12 +1205,44 @@ function MoneyRequestConfirmationListFooter({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRoute()));
}}
- style={styles.expenseViewImageSmall}
+ style={isCompactMode ? receiptHeightStyle : [styles.expenseViewImageSmall, styles.receiptPreviewAspectRatio]}
/>
)}
)}
- {fields.filter((field) => field.shouldShow).map((field) => field.item)}
+
+
+ {isCompactMode && (
+
+
+ {translate('iou.automaticallyEnterExpenseDetails')}
+
+ )}
+
+ {fields.filter((field) => field.shouldShow && (field.shouldShowAboveShowMore ?? false)).map((field) => field.item)}
+
+ {!isCompactMode && fields.filter((field) => field.shouldShow && !(field.shouldShowAboveShowMore ?? false)).map((field) => {field.item})}
+
+ {isCompactMode && fields.some((field) => field.shouldShow && !(field.shouldShowAboveShowMore ?? false)) && (
+
+
+
+ )}
+
>
);
}
@@ -1179,5 +1292,6 @@ export default memo(
prevProps.unit === nextProps.unit &&
prevProps.isTimeRequest === nextProps.isTimeRequest &&
prevProps.iouTimeCount === nextProps.iouTimeCount &&
- prevProps.iouTimeRate === nextProps.iouTimeRate,
+ prevProps.iouTimeRate === nextProps.iouTimeRate &&
+ prevProps.showMoreFields === nextProps.showMoreFields,
);
diff --git a/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.native.ts b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.native.ts
new file mode 100644
index 0000000000000..e85818468d844
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.native.ts
@@ -0,0 +1,24 @@
+import variables from '@styles/variables';
+import type GetImageCompactModeStyle from './types';
+
+const getImageCompactModeStyle: GetImageCompactModeStyle = (maxWidth, availableWidth, aspectRatio) => {
+ const fullWidthLimit = availableWidth / variables.receiptPreviewMaxHeight;
+ const isTall = aspectRatio && aspectRatio <= fullWidthLimit;
+
+ // Cap ratio to 16:9 so wide images don't stretch outside the bounds of the screen.
+ const cappedRatio = aspectRatio ? Math.min(aspectRatio, 16 / 9) : 16 / 9;
+
+ return {
+ width: '100%',
+ maxWidth,
+ minHeight: 180,
+ maxHeight: variables.receiptPreviewMaxHeight,
+ aspectRatio: isTall ? undefined : cappedRatio,
+ height: isTall ? variables.receiptPreviewMaxHeight : 'auto',
+ flexShrink: 1,
+ alignSelf: 'center',
+ marginHorizontal: 0,
+ };
+};
+
+export default getImageCompactModeStyle;
diff --git a/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.ts b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.ts
new file mode 100644
index 0000000000000..c7401c5240768
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/index.ts
@@ -0,0 +1,15 @@
+import type GetImageCompactModeStyle from './types';
+
+const getImageCompactModeStyle: GetImageCompactModeStyle = (maxWidth) => {
+ return {
+ maxWidth,
+ minHeight: 180,
+ flexShrink: 1,
+ alignSelf: 'center',
+ width: '100%',
+ marginHorizontal: 0,
+ height: 'auto',
+ };
+};
+
+export default getImageCompactModeStyle;
diff --git a/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/types.ts b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/types.ts
new file mode 100644
index 0000000000000..846a5fbb1231e
--- /dev/null
+++ b/src/components/MoneyRequestConfirmationListFooter/getImageCompactModeStyle/types.ts
@@ -0,0 +1,5 @@
+import type {ViewStyle} from 'react-native';
+
+type GetImageCompactModeStyle = (maxWidth: number, availableWidth: number, aspectRatio?: number | null, imageHeight?: number | null) => ViewStyle;
+
+export default GetImageCompactModeStyle;
diff --git a/src/components/ReceiptImage/index.tsx b/src/components/ReceiptImage/index.tsx
index 32c3561fb3d35..0c60eea4abfbb 100644
--- a/src/components/ReceiptImage/index.tsx
+++ b/src/components/ReceiptImage/index.tsx
@@ -1,5 +1,5 @@
import React, {useRef, useState} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
+import type {ImageResizeMode, ImageStyle, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import EReceiptThumbnail from '@components/EReceiptThumbnail';
import type {IconSize} from '@components/EReceiptThumbnail';
@@ -122,8 +122,14 @@ type ReceiptImageProps = (
/** Callback to be called when the image fails to load */
onLoadFailure?: () => void;
+ /** The resize mode of the image */
+ resizeMode?: ImageResizeMode;
+
/** Whether the receipt is a map distance request */
isMapDistanceRequest?: boolean;
+
+ /** Any additional styles to apply */
+ style?: StyleProp;
};
function ReceiptImage({
@@ -151,7 +157,9 @@ function ReceiptImage({
thumbnailContainerStyles,
onLoad,
onLoadFailure,
+ resizeMode,
isMapDistanceRequest,
+ style,
}: ReceiptImageProps) {
const styles = useThemeStyles();
const [receiptImageWidth, setReceiptImageWidth] = useState(undefined);
@@ -193,7 +201,7 @@ function ReceiptImage({
if (isThumbnail || (isEReceipt && isPerDiemRequest)) {
const props = isThumbnail && {fileExtension, isReceiptThumbnail: true};
return (
-
+
);
}
@@ -233,7 +242,7 @@ function ReceiptImage({
lastUpdateWidthTimestampRef.current = e.timeStamp;
}}
source={typeof source === 'string' ? {uri: source} : source}
- style={isMapDistanceRequest && styles.flex1}
+ style={[style, isMapDistanceRequest && styles.flex1, styles.overflowHidden]}
isAuthTokenRequired={!!isAuthTokenRequired}
loadingIconSize={loadingIconSize}
loadingIndicatorStyles={loadingIndicatorStyles}
@@ -243,6 +252,7 @@ function ReceiptImage({
shouldCalculateAspectRatioForWideImage={shouldUseFullHeight}
imageWidthToCalculateHeight={receiptImageWidth}
onError={onLoadFailure}
+ resizeMode={resizeMode}
/>
);
}
diff --git a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
index 24020a1e79883..3bbb75de785f6 100644
--- a/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
+++ b/src/components/SelectionListWithSections/BaseSelectionListWithSections.tsx
@@ -144,6 +144,7 @@ function BaseSelectionListWithSections({
renderScrollComponent,
shouldShowRightCaret,
shouldHighlightSelectedItem = true,
+ ListFooterComponentStyle,
shouldDisableHoverStyle = false,
setShouldDisableHoverStyle = () => {},
shouldSkipContentHeaderHeightOffset,
@@ -632,6 +633,7 @@ function BaseSelectionListWithSections({
disabled={flattenedSections.allOptions.length === flattenedSections.disabledOptionsIndexes.length}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined}
+ sentryLabel={CONST.SENTRY_LABEL.SELECTION_LIST_WITH_SECTIONS.SELECT_ALL}
>
{translate('workspace.people.selectAll')}
@@ -1079,6 +1081,7 @@ function BaseSelectionListWithSections({
{listFooterContent}
>
}
+ ListFooterComponentStyle={ListFooterComponentStyle}
onEndReached={handleOnEndReached}
onEndReachedThreshold={onEndReachedThreshold}
scrollEventThrottle={scrollEventThrottle}
diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts
index 03ecfb999f286..7c67cd9ae2066 100644
--- a/src/components/SelectionListWithSections/types.ts
+++ b/src/components/SelectionListWithSections/types.ts
@@ -1126,6 +1126,9 @@ type SelectionListProps = Partial & {
/** Whether to highlight the selected item */
shouldHighlightSelectedItem?: boolean;
+ /** Styles to apply to the list footer component */
+ ListFooterComponentStyle?: StyleProp;
+
/** Whether hover style should be disabled */
shouldDisableHoverStyle?: boolean;
setShouldDisableHoverStyle?: React.Dispatch>;
diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx
index 1a440e61a338b..8ab7001505404 100644
--- a/src/components/ThumbnailImage.tsx
+++ b/src/components/ThumbnailImage.tsx
@@ -1,5 +1,5 @@
import React, {useCallback, useEffect, useState} from 'react';
-import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
+import type {ImageResizeMode, ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useNetwork from '@hooks/useNetwork';
@@ -76,6 +76,9 @@ type ThumbnailImageProps = {
/** Callback to be called when the image loads */
onLoad?: (event: {nativeEvent: {width: number; height: number}}) => void;
+
+ /** The resize mode of the image */
+ resizeMode?: ImageResizeMode;
};
function ThumbnailImage({
@@ -97,6 +100,7 @@ function ThumbnailImage({
onMeasure,
loadingIndicatorStyles,
onLoad,
+ resizeMode,
}: ThumbnailImageProps) {
const icons = useMemoizedLazyExpensifyIcons(['Gallery', 'OfflineCloud']);
const styles = useThemeStyles();
@@ -161,6 +165,7 @@ function ThumbnailImage({
{
updateImageSize(args);
onMeasure?.();
@@ -174,6 +179,7 @@ function ThumbnailImage({
loadingIconSize={loadingIconSize}
loadingIndicatorStyles={loadingIndicatorStyles}
onLoad={onLoad}
+ resizeMode={resizeMode}
/>
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 15ec5a963a917..87130c1e1f7b4 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Karte',
whyDoWeAskForThis: 'Warum fragen wir danach?',
required: 'Erforderlich',
+ automatic: 'Automatisch',
showing: 'Wird angezeigt',
of: 'von',
default: 'Standard',
@@ -1225,6 +1226,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Beleg wartet auf Abgleich mit Kartenumsatz. Als Barzahlung markieren, um abzubrechen.',
markAsCash: 'Als Bar markieren',
routePending: 'Routing ausstehend ...',
+ automaticallyEnterExpenseDetails: 'Concierge wird automatisch die Ausgabendetails für Sie eingeben, oder Sie können sie manuell hinzufügen.',
receiptScanning: () => ({
one: 'Beleg wird gescannt ...',
other: 'Belege werden gescannt …',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 16b2b60d018fa..c954965aa06ac 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -489,6 +489,7 @@ const translations = {
card: 'Card',
whyDoWeAskForThis: 'Why do we ask for this?',
required: 'Required',
+ automatic: 'Automatic',
showing: 'Showing',
of: 'of',
default: 'Default',
@@ -1239,6 +1240,7 @@ const translations = {
pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.',
markAsCash: 'Mark as cash',
routePending: 'Route pending...',
+ automaticallyEnterExpenseDetails: 'Concierge will automatically enter the expense details for you, or you can add them manually.',
receiptScanning: () => ({
one: 'Receipt scanning...',
other: 'Receipts scanning...',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 71b1bc89c1b5d..d1b08778322c0 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -260,6 +260,7 @@ const translations: TranslationDeepObject = {
card: 'Tarjeta',
whyDoWeAskForThis: '¿Por qué pedimos esto?',
required: 'Obligatorio',
+ automatic: 'Automático',
showing: 'Mostrando',
of: 'de',
default: 'Predeterminado',
@@ -992,6 +993,7 @@ const translations: TranslationDeepObject = {
other: 'Problemas encontrados',
}),
fieldPending: 'Pendiente...',
+ automaticallyEnterExpenseDetails: 'Concierge introducirá automáticamente los detalles del gasto por ti, o puedes añadirlos manualmente.',
receiptScanning: () => ({
one: 'Escaneando recibo...',
other: 'Escaneando recibos...',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index ce7dad2ed6e47..e1921fd2a9858 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Carte',
whyDoWeAskForThis: 'Pourquoi demandons-nous cela ?',
required: 'Obligatoire',
+ automatic: 'Automatique',
showing: 'Affichage',
of: 'de',
default: 'Par défaut',
@@ -1230,6 +1231,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Reçu en attente de rapprochement avec une transaction par carte. Marquez comme paiement en espèces pour annuler.',
markAsCash: 'Marquer comme espèces',
routePending: 'Acheminement en attente...',
+ automaticallyEnterExpenseDetails: 'Concierge saisira automatiquement les détails de la dépense pour vous, ou vous pouvez les ajouter manuellement.',
receiptScanning: () => ({
one: 'Scan des reçus...',
other: 'Scan des reçus...',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 3ba557e6981dc..638269b8315d8 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Carta',
whyDoWeAskForThis: 'Perché lo chiediamo?',
required: 'Obbligatorio',
+ automatic: 'Automatico',
showing: 'Mostrando',
of: 'di',
default: 'Predefinito',
@@ -1223,6 +1224,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Ricevuta in attesa di abbinamento con la transazione della carta. Contrassegna come contante per annullare.',
markAsCash: 'Segna come contante',
routePending: 'Instradamento in sospeso...',
+ automaticallyEnterExpenseDetails: 'Concierge inserirà automaticamente i dettagli della spesa per te, oppure puoi aggiungerli manualmente.',
receiptScanning: () => ({
one: 'Scansione della ricevuta in corso...',
other: 'Scansione delle ricevute in corso...',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index c7239f73add02..7effbc6d8eab4 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'カード',
whyDoWeAskForThis: 'なぜこの情報が必要なのですか?',
required: '必須',
+ automatic: '自動',
showing: '表示中',
of: 'の',
default: 'デフォルト',
@@ -1219,6 +1220,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'レシートはカード取引との照合待ちです。現金としてマークしてキャンセルします。',
markAsCash: '現金としてマーク',
routePending: 'ルート保留中…',
+ automaticallyEnterExpenseDetails: 'コンシェルジュが自動的に経費の詳細を入力するか、手動で追加することができます。',
receiptScanning: () => ({
one: 'レシートをスキャンしています…',
other: 'レシートをスキャンしています…',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 14f4a9ca75e95..a54862e002a15 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Kaart',
whyDoWeAskForThis: 'Waarom vragen we dit?',
required: 'Vereist',
+ automatic: 'Automatisch',
showing: 'Wordt weergegeven',
of: 'of',
default: 'Standaard',
@@ -1223,6 +1224,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Bon wordt nog gekoppeld aan kaarttransactie. Markeer als contant om te annuleren.',
markAsCash: 'Markeren als contant',
routePending: 'Routeren in behandeling...',
+ automaticallyEnterExpenseDetails: 'Concierge zal automatisch de uitgavendetails voor je invoeren, of je kunt ze handmatig toevoegen.',
receiptScanning: () => ({
one: 'Bon wordt gescand...',
other: 'Bonnetjes scannen...',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 2fd67d87964a8..cc73cd73a734e 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Karta',
whyDoWeAskForThis: 'Dlaczego o to prosimy?',
required: 'Wymagane',
+ automatic: 'Automatyczny',
showing: 'Wyświetlanie',
of: 'z',
default: 'Domyślne',
@@ -1223,6 +1224,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Oczekuje na dopasowanie paragonu do transakcji kartą. Oznacz jako gotówkę, aby anulować.',
markAsCash: 'Oznacz jako gotówkę',
routePending: 'Trasa w toku…',
+ automaticallyEnterExpenseDetails: 'Concierge automatycznie wprowadzi szczegóły wydatku za Ciebie lub możesz dodać je ręcznie.',
receiptScanning: () => ({
one: 'Skanowanie paragonu...',
other: 'Skanowanie paragonów...',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index dcf8b53ec44d6..676b9b61a5911 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -477,6 +477,7 @@ const translations: TranslationDeepObject = {
card: 'Cartão',
whyDoWeAskForThis: 'Por que pedimos isso?',
required: 'Obrigatório',
+ automatic: 'Automático',
showing: 'Mostrando',
of: 'de',
default: 'Padrão',
@@ -1222,6 +1223,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: 'Recibo aguardando correspondência com transação do cartão. Marque como dinheiro para cancelar.',
markAsCash: 'Marcar como dinheiro',
routePending: 'Rota pendente...',
+ automaticallyEnterExpenseDetails: 'O Concierge inserirá automaticamente os detalhes da despesa para você, ou você pode adicioná-los manualmente.',
receiptScanning: () => ({
one: 'Digitalizando recibo...',
other: 'Escaneando recibos...',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 22a50a225cdd2..b03b3e1768544 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -478,6 +478,7 @@ const translations: TranslationDeepObject = {
whyDoWeAskForThis: '我们为什么要询问这个?',
required: '必填',
showing: '正在显示',
+ automatic: '自动',
of: '的',
default: '默认',
update: '更新',
@@ -1203,6 +1204,7 @@ const translations: TranslationDeepObject = {
pendingMatchWithCreditCardDescription: '收据正在等待与卡片交易匹配。将其标记为现金以取消。',
markAsCash: '标记为现金',
routePending: '路由处理中…',
+ automaticallyEnterExpenseDetails: 'Concierge 将自动为您输入费用详情,或者您可以手动添加。',
receiptScanning: () => ({
one: '正在扫描收据…',
other: '正在扫描收据…',
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index ffa0471c21860..1833235578d24 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -2668,6 +2668,21 @@ function createUnreportedExpenses(transactions: Array):
);
}
+function willFieldBeAutomaticallyFilled(transaction: OnyxEntry, fieldType: 'amount' | 'merchant' | 'date' | 'category'): boolean {
+ if (!transaction?.receipt) {
+ return false;
+ }
+
+ const isSmartScanActive = isScanRequest(transaction);
+
+ if (!isSmartScanActive) {
+ return false;
+ }
+
+ const autoFillableFields = ['amount', 'merchant', 'date', 'category'];
+ return autoFillableFields.includes(fieldType);
+}
+
function isExpenseUnreported(transaction?: Transaction): transaction is UnreportedTransaction {
return transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
}
@@ -2778,6 +2793,7 @@ export {
isCreatedMissing,
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
+ willFieldBeAutomaticallyFilled,
hasPendingRTERViolation,
hasValidModifiedAmount,
allHavePendingRTERViolation,
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 37fc2d231459f..b44d394c1bd69 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -4331,7 +4331,6 @@ const staticStyles = (theme: ThemeColors) =>
},
moneyRequestImage: {
- height: 200,
borderRadius: 16,
marginHorizontal: 20,
overflow: 'hidden',
@@ -4527,7 +4526,7 @@ const staticStyles = (theme: ThemeColors) =>
height: 'auto',
},
expenseViewImageSmall: {
- maxWidth: 440,
+ maxWidth: variables.receiptPreviewMaxWidth,
aspectRatio: 16 / 9,
height: 'auto',
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index b2cf557fe00ae..8d3334ba6a423 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -104,6 +104,8 @@ export default {
sideBarWidth: 375,
sidePanelWidth: 375,
receiptPaneRHPMaxWidth: 465,
+ receiptPreviewMaxWidth: 440,
+ receiptPreviewMaxHeight: 440,
homePageLeftColumnMaxWidth: 680,
homePageRightColumnMaxWidth: 488,
superWideRHPMaxWidth: 1260,
diff --git a/tests/ui/MoneyRequestReportFooter.tsx b/tests/ui/MoneyRequestReportFooter.tsx
index 8363694cf7179..2cc0d8a69b904 100644
--- a/tests/ui/MoneyRequestReportFooter.tsx
+++ b/tests/ui/MoneyRequestReportFooter.tsx
@@ -123,6 +123,8 @@ const renderMoneyRequestConfirmationListFooter = (transaction: Transaction) => {
iouTimeCount: undefined,
iouTimeRate: undefined,
isTimeRequest: false,
+ showMoreFields: false,
+ setShowMoreFields: jest.fn(),
};
return render(