Skip to content

Commit 20f2fef

Browse files
authored
Merge pull request Expensify#94545 from Expensify/claude-fixDateRequiredNewManualExpenseFlow
Require date in new manual expense flow for distance, time, and invoice
2 parents 9d7d0a6 + ca11f36 commit 20f2fef

5 files changed

Lines changed: 121 additions & 3 deletions

File tree

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
1616
import {isCategoryDescriptionRequired} from '@libs/CategoryUtils';
1717
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
1818
import {isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils';
19+
import {shouldShowConfirmationDate} from '@libs/MoneyRequestUtils';
1920
import Navigation from '@libs/Navigation/Navigation';
2021
import {hasEnabledOptions} from '@libs/OptionsListUtils';
2122
import {arePolicyRulesEnabled, isTaxTrackingEnabled} from '@libs/PolicyUtils';
@@ -477,6 +478,8 @@ function MoneyRequestConfirmationList({
477478
isTimeRequest,
478479
routeError,
479480
isNewManualExpenseFlowEnabled,
481+
isReadOnly,
482+
shouldShowDate: shouldShowConfirmationDate(shouldShowSmartScanFields, isDistanceRequest),
480483
});
481484

482485
const confirm = buildConfirmAction({

src/components/MoneyRequestConfirmationList/hooks/useConfirmationValidation.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ type UseConfirmationValidationParams = {
110110

111111
/** Whether the new manual expense flow is enabled */
112112
isNewManualExpenseFlowEnabled: boolean;
113+
114+
/** Whether the confirmation fields are read-only (date is not inline-editable) */
115+
isReadOnly: boolean;
116+
117+
/** Whether the date field is shown for this flow (mirrors the footer's date visibility) */
118+
shouldShowDate: boolean;
113119
};
114120

115121
/**
@@ -154,6 +160,8 @@ function useConfirmationValidation({
154160
isTimeRequest,
155161
routeError,
156162
isNewManualExpenseFlowEnabled,
163+
isReadOnly,
164+
shouldShowDate,
157165
}: UseConfirmationValidationParams): {validate: (paymentType?: PaymentMethodType) => ValidationResult | null} {
158166
const {getCurrencyDecimals} = useCurrencyListActions();
159167
const selectedParticipantsCount = selectedParticipants.length;
@@ -189,8 +197,11 @@ function useConfirmationValidation({
189197
) {
190198
return {errorKey: 'common.error.invalidAmount'};
191199
}
192-
// The date is an inline required field in the new manual flow; block confirmation when the user cleared it.
193-
if (isNewManualExpenseFlowEnabled && transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.MANUAL && isCreatedMissing(transaction)) {
200+
// The date is an inline, clearable required field in the new manual flow for every type that shows it
201+
// (manual, distance, time, invoice, ...). Block confirmation when the user cleared it. Gating on the same
202+
// `shouldShowDate && !isReadOnly` condition that renders the inline picker keeps validation and UI in sync,
203+
// and skips read-only/scan flows where the date is populated server-side.
204+
if (isNewManualExpenseFlowEnabled && shouldShowDate && !isReadOnly && isCreatedMissing(transaction)) {
194205
return {errorKey: 'common.error.fieldRequired'};
195206
}
196207
const merchantValue = iouMerchant ?? '';

src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {derivedFlagsSliceSelector} from '@components/MoneyRequestConfirmationLis
44
import useTransactionSelector from '@components/MoneyRequestConfirmationList/sections/useTransactionSelector';
55
import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
66
import {isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
7+
import {shouldShowConfirmationDate} from '@libs/MoneyRequestUtils';
78
import {hasEnabledTags} from '@libs/TagsOptionsListUtils';
89
import {getCurrency, isManagedCardTransaction, isScanRequest, shouldShowAttendees as shouldShowAttendeesTransactionUtils} from '@libs/TransactionUtils';
910
import CONST from '@src/CONST';
@@ -79,7 +80,7 @@ function useFooterDerivedFlags({
7980

8081
// In Send Money and Split Bill with Scan flow, we don't allow the Merchant or Date to be edited.
8182
// For distance requests, don't show the merchant as there's already another "Distance" menu item.
82-
const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest;
83+
const shouldShowDate = shouldShowConfirmationDate(shouldShowSmartScanFields, isDistanceRequest);
8384

8485
// Determines whether the tax fields can be modified.
8586
// The tax fields can only be modified if the component is not in read-only mode

src/libs/MoneyRequestUtils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,18 @@ function isValidMerchant(merchant: string | undefined, transaction?: OnyxEntry<T
206206
return valueByteLength <= CONST.MERCHANT_NAME_MAX_BYTES;
207207
}
208208

209+
/**
210+
* Determines whether the date field should be shown on the money request confirmation surface.
211+
* This is the single source of truth shared by the confirmation footer (where the date field is rendered)
212+
* and the confirmation-step validation (where a missing date is blocked), so the two never drift out of sync.
213+
*/
214+
function shouldShowConfirmationDate(shouldShowSmartScanFields: boolean, isDistanceRequest: boolean): boolean {
215+
return shouldShowSmartScanFields || isDistanceRequest;
216+
}
217+
209218
export {
210219
addLeadingZero,
220+
shouldShowConfirmationDate,
211221
replaceAllDigits,
212222
stripCommaFromAmount,
213223
stripDecimalsFromAmount,

tests/unit/hooks/useConfirmationValidation.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ const baseParams = {
9797
isTimeRequest: false,
9898
routeError: undefined,
9999
isNewManualExpenseFlowEnabled: false,
100+
isReadOnly: false,
101+
shouldShowDate: true,
100102
} satisfies UseConfirmationValidationParams;
101103

102104
function createValidationParamsForParticipant(
@@ -746,4 +748,95 @@ describe('useConfirmationValidation', () => {
746748
expect(result.current.validate()).toEqual({errorKey: 'iou.error.invalidAmount'});
747749
});
748750
});
751+
752+
describe('date validation — inline required date in new manual expense flow', () => {
753+
const newManualFlowParams = {
754+
...baseParams,
755+
isNewManualExpenseFlowEnabled: true,
756+
};
757+
758+
it('returns fieldRequired for manual expense when the date is removed', () => {
759+
const {result} = renderHook(() =>
760+
useConfirmationValidation(createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, newManualFlowParams, {created: '', isAmountSet: true})),
761+
);
762+
expect(result.current.validate()).toEqual({errorKey: 'common.error.fieldRequired'});
763+
});
764+
765+
it('returns fieldRequired for distance expense when the date is removed', () => {
766+
const {result} = renderHook(() =>
767+
useConfirmationValidation({
768+
...newManualFlowParams,
769+
isDistanceRequest: true,
770+
transaction: createTransactionBase({
771+
amount: 1000,
772+
created: '',
773+
iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE,
774+
participants: [POLICY_EXPENSE_CHAT_PARTICIPANT],
775+
comment: {type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT},
776+
}),
777+
selectedParticipants: [POLICY_EXPENSE_CHAT_PARTICIPANT],
778+
}),
779+
);
780+
expect(result.current.validate()).toEqual({errorKey: 'common.error.fieldRequired'});
781+
});
782+
783+
it('returns fieldRequired for time expense when the date is removed', () => {
784+
const {result} = renderHook(() =>
785+
useConfirmationValidation({
786+
...newManualFlowParams,
787+
isTimeRequest: true,
788+
transaction: createTransactionBase({
789+
amount: 1000,
790+
created: '',
791+
iouRequestType: CONST.IOU.REQUEST_TYPE.TIME,
792+
participants: [POLICY_EXPENSE_CHAT_PARTICIPANT],
793+
comment: {type: CONST.TRANSACTION.TYPE.TIME, units: {count: 1, rate: 100}},
794+
}),
795+
selectedParticipants: [POLICY_EXPENSE_CHAT_PARTICIPANT],
796+
}),
797+
);
798+
expect(result.current.validate()).toEqual({errorKey: 'common.error.fieldRequired'});
799+
});
800+
801+
it('returns fieldRequired for invoice when the date is removed', () => {
802+
const {result} = renderHook(() =>
803+
useConfirmationValidation(
804+
createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, {...newManualFlowParams, iouType: CONST.IOU.TYPE.INVOICE}, {created: '', isAmountSet: true}),
805+
),
806+
);
807+
expect(result.current.validate()).toEqual({errorKey: 'common.error.fieldRequired'});
808+
});
809+
810+
it('does not return fieldRequired when the date is present', () => {
811+
const {result} = renderHook(() =>
812+
useConfirmationValidation(createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, newManualFlowParams, {created: '2025-01-15', isAmountSet: true})),
813+
);
814+
expect(result.current.validate()).toEqual({errorKey: null});
815+
});
816+
817+
it('does not return fieldRequired when the fields are read-only (date populated server-side)', () => {
818+
const {result} = renderHook(() =>
819+
useConfirmationValidation(
820+
createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, {...newManualFlowParams, isReadOnly: true}, {created: '', isAmountSet: true}),
821+
),
822+
);
823+
expect(result.current.validate()).toEqual({errorKey: null});
824+
});
825+
826+
it('does not return fieldRequired when the date field is not shown (pure scan flow)', () => {
827+
const {result} = renderHook(() =>
828+
useConfirmationValidation(
829+
createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, {...newManualFlowParams, shouldShowDate: false}, {created: '', isAmountSet: true}),
830+
),
831+
);
832+
expect(result.current.validate()).toEqual({errorKey: null});
833+
});
834+
835+
it('does not return fieldRequired when the new manual expense flow beta is disabled', () => {
836+
const {result} = renderHook(() =>
837+
useConfirmationValidation(createValidationParamsForParticipant(POLICY_EXPENSE_CHAT_PARTICIPANT, {isNewManualExpenseFlowEnabled: false}, {created: '', isAmountSet: true})),
838+
);
839+
expect(result.current.validate()).toEqual({errorKey: null});
840+
});
841+
});
749842
});

0 commit comments

Comments
 (0)