From bf030ac46fc7d1e62f3b008a72ceca4279cf0f81 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 7 Oct 2025 20:45:24 +0800 Subject: [PATCH 01/14] feat: add sparkles --- assets/images/sparkles.svg | 6 +++ src/components/Icon/Expensicons.ts | 2 + src/components/MenuItem.tsx | 14 +++++- .../MoneyRequestConfirmationListFooter.tsx | 44 +++++++++++++++++-- src/languages/en.ts | 3 ++ src/languages/es.ts | 2 + src/libs/TransactionUtils/index.ts | 16 +++++++ 7 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 assets/images/sparkles.svg diff --git a/assets/images/sparkles.svg b/assets/images/sparkles.svg new file mode 100644 index 0000000000000..888c5f6a514f0 --- /dev/null +++ b/assets/images/sparkles.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 16fc15267b40a..9b9c13afeb938 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -211,6 +211,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'; @@ -412,6 +413,7 @@ export { Send, Shield, SmartScan, + Sparkles, Stopwatch, Suitcase, Sync, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 5c3b82410b9b2..12d13dab4319f 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -196,6 +196,9 @@ type MenuItemBaseProps = { /** 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; @@ -449,6 +452,7 @@ function MenuItem({ titleContainerStyle, subtitle, shouldShowBasicTitle, + rightLabelIcon, label, shouldTruncateTitle = false, characterLimit = 200, @@ -934,7 +938,15 @@ function MenuItem({ )} {!title && !!rightLabel && !errorText && ( - + + {!!rightLabelIcon && ( + + )} {rightLabel} )} diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 29974e5742a05..f475767f3cb5a 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -2,14 +2,15 @@ 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 {View} from 'react-native'; +import React, {memo, useCallback, useMemo} from 'react'; +import {Text, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -39,9 +40,11 @@ import { isCreatedMissing, isFetchingWaypointsFromServer, 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'; @@ -53,6 +56,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Badge from './Badge'; import ConfirmedRoute from './ConfirmedRoute'; import MentionReportContext from './HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; +import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; @@ -273,6 +277,7 @@ function MoneyRequestConfirmationListFooter({ const styles = useThemeStyles(); const {translate, toLocaleDigit, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); + const theme = useTheme(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); @@ -412,6 +417,26 @@ function MoneyRequestConfirmationListFooter({ const mentionReportContextValue = useMemo(() => ({currentReportID: reportID, exactlyMatch: true}), [reportID]); + const getRightLabelIcon = useCallback( + (fieldType: 'amount' | 'merchant' | 'date' | 'category') => { + return willFieldBeAutomaticallyFilled(transaction, fieldType) ? Expensicons.Sparkles : undefined; + }, + [transaction], + ); + + const getRightLabel = useCallback( + (fieldType: 'amount' | 'merchant' | 'date' | 'category', isRequiredField = false) => { + if (willFieldBeAutomaticallyFilled(transaction, fieldType)) { + return translate('common.automatic'); + } + if (isRequiredField) { + return translate('common.required'); + } + return ''; + }, + [transaction, translate], + ); + const fields = [ { item: ( @@ -555,7 +580,8 @@ function MoneyRequestConfirmationListFooter({ interactive={!isReadOnly} brickRoadIndicator={shouldDisplayMerchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={shouldDisplayMerchantError ? translate('common.error.fieldRequired') : ''} - rightLabel={isMerchantRequired && !shouldDisplayMerchantError ? translate('common.required') : ''} + rightLabel={getRightLabel('merchant', !!isMerchantRequired && !shouldDisplayMerchantError)} + rightLabelIcon={getRightLabelIcon('merchant')} numberOfLinesTitle={2} /> ), @@ -580,7 +606,8 @@ function MoneyRequestConfirmationListFooter({ titleStyle={styles.flex1} disabled={didConfirm} interactive={!isReadOnly} - rightLabel={isCategoryRequired ? translate('common.required') : ''} + rightLabel={getRightLabel('category', isCategoryRequired)} + rightLabelIcon={getRightLabelIcon('category')} /> ), shouldShow: shouldShowCategories, @@ -1008,6 +1035,15 @@ function MoneyRequestConfirmationListFooter({ style={styles.expenseViewImageSmall} /> )} + + + {translate('iou.automaticallyEnterExpenseDetails')} + )} {fields.filter((field) => field.shouldShow).map((field) => field.item)} diff --git a/src/languages/en.ts b/src/languages/en.ts index b07bebc41c22a..d5013ed4e6e31 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -553,6 +553,7 @@ const translations = { card: 'Card', whyDoWeAskForThis: 'Why do we ask for this?', required: 'Required', + automatic: 'Automatic', showing: 'Showing', of: 'of', default: 'Default', @@ -783,6 +784,7 @@ const translations = { moneyRequestConfirmationList: { paidBy: 'Paid by', whatsItFor: "What's it for?", + smartScanExplainer: 'Concierge will automatically enter the expense details for you, or you can add them manually.', }, selectionList: { nameEmailOrPhoneNumber: 'Name, email, or phone number', @@ -1133,6 +1135,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 f892f0dc9206e..dab554319ede7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -543,6 +543,7 @@ const translations = { card: 'Tarjeta', whyDoWeAskForThis: '¿Por qué pedimos esto?', required: 'Obligatorio', + automatic: 'Automático', showing: 'Mostrando', of: 'de', default: 'Predeterminado', @@ -770,6 +771,7 @@ const translations = { moneyRequestConfirmationList: { paidBy: 'Pagado por', whatsItFor: '¿Para qué es?', + smartScanExplainer: 'Concierge introducirá automáticamente los detalles del gasto por ti, o puedes añadirlos manualmente.', }, selectionList: { nameEmailOrPhoneNumber: 'Nombre, correo electrónico o número de teléfono', diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index b89f1aec0b3a4..a039219671039 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1950,6 +1950,21 @@ function createUnreportedExpenseSections(transactions: Array, 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); +} + // Temporarily only for use in the Unreported Expense project function isExpenseUnreported(transaction?: Transaction): transaction is UnreportedTransaction { // TODO: added for development purposes, should be removed once the feature are fully implemented @@ -2022,6 +2037,7 @@ export { isCreatedMissing, areRequiredFieldsEmpty, hasMissingSmartscanFields, + willFieldBeAutomaticallyFilled, hasPendingRTERViolation, allHavePendingRTERViolation, hasPendingUI, From 0bb3cc2276902cf0ea1bff62247a6580d220d2d7 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Tue, 7 Oct 2025 21:05:09 +0800 Subject: [PATCH 02/14] feat: add show more fields --- .../MoneyRequestConfirmationListFooter.tsx | 72 +++++++++++++++---- src/styles/index.ts | 5 ++ 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index f475767f3cb5a..e6e58fb8cfa82 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -2,8 +2,8 @@ import {emailSelector} from '@selectors/Session'; import {format} from 'date-fns'; import {Str} from 'expensify-common'; import {deepEqual} from 'fast-equals'; -import React, {memo, useCallback, useMemo} from 'react'; -import {Text, View} from 'react-native'; +import React, {memo, useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; @@ -39,6 +39,7 @@ import { isCardTransaction, isCreatedMissing, isFetchingWaypointsFromServer, + isScanRequest, shouldShowAttendees as shouldShowAttendeesTransactionUtils, willFieldBeAutomaticallyFilled, } from '@libs/TransactionUtils'; @@ -54,6 +55,7 @@ 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'; @@ -65,6 +67,7 @@ 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 */ @@ -279,6 +282,8 @@ function MoneyRequestConfirmationListFooter({ const {isOffline} = useNetwork(); const theme = useTheme(); + const [showMoreFields, setShowMoreFields] = useState(false); + 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}); @@ -463,6 +468,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowSmartScanFields && shouldShowAmountField, + isRequired: true, }, { item: ( @@ -495,6 +501,7 @@ function MoneyRequestConfirmationListFooter({ ), shouldShow: true, + isRequired: true, }, { item: ( @@ -522,6 +529,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: isDistanceRequest, + isRequired: true, // Distance is required for distance requests }, { item: ( @@ -559,6 +567,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: isDistanceRequest, + isRequired: false, }, { item: ( @@ -586,6 +595,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowMerchant, + isRequired: false, }, { item: ( @@ -611,6 +621,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowCategories, + isRequired: false, }, { item: ( @@ -636,6 +647,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowDate, + isRequired: false, }, ...policyTagLists.map(({name}, index) => { const tagVisibilityItem = tagVisibility.at(index); @@ -667,6 +679,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow, + isRequired: false, }; }), { @@ -690,6 +703,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowTax, + isRequired: false, }, { item: ( @@ -712,6 +726,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowTax, + isRequired: false, }, { item: ( @@ -736,6 +751,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: shouldShowAttendees, + isRequired: false, }, { item: ( @@ -755,6 +771,7 @@ function MoneyRequestConfirmationListFooter({ ), shouldShow: shouldShowReimbursable, isSupplementary: true, + isRequired: false, }, { item: ( @@ -773,6 +790,7 @@ function MoneyRequestConfirmationListFooter({ ), shouldShow: shouldShowBillable, + isRequired: false, }, { item: ( @@ -794,6 +812,7 @@ function MoneyRequestConfirmationListFooter({ /> ), shouldShow: isPolicyExpenseChat, + isRequired: false, }, ]; @@ -858,7 +877,7 @@ function MoneyRequestConfirmationListFooter({ const receiptThumbnailContent = useMemo( () => ( - + {isLocalFile && Str.isPDF(receiptFilename) ? ( { @@ -922,9 +941,11 @@ function MoneyRequestConfirmationListFooter({ [ styles.moneyRequestImage, styles.expenseViewImageSmall, + styles.moneyRequestImageLarge, styles.cursorDefault, styles.h100, styles.flex1, + showMoreFields, isLocalFile, receiptFilename, translate, @@ -938,10 +959,10 @@ function MoneyRequestConfirmationListFooter({ fileExtension, isDistanceRequest, transactionID, + isReceiptEditable, + reportID, action, iouType, - reportID, - isReceiptEditable, ], ); @@ -1035,18 +1056,39 @@ function MoneyRequestConfirmationListFooter({ style={styles.expenseViewImageSmall} /> )} - - - {translate('iou.automaticallyEnterExpenseDetails')} - + {isScanRequest(transaction) && ( + + + {translate('iou.automaticallyEnterExpenseDetails')} + + )} )} - {fields.filter((field) => field.shouldShow).map((field) => field.item)} + + {fields.filter((field) => field.shouldShow && (field.isRequired ?? false)).map((field) => field.item)} + + {showMoreFields && fields.filter((field) => field.shouldShow && !(field.isRequired ?? false)).map((field) => field.item)} + + {!showMoreFields && fields.some((field) => field.shouldShow && !(field.isRequired ?? false)) && ( + + +