Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
58451eb
Add beta
ChavdaSachin Feb 27, 2026
bc70131
replace amount pushROw with inline input
ChavdaSachin Feb 27, 2026
88fa1f4
IOU confirmation - amount - replace push row with inline input
ChavdaSachin Mar 9, 2026
f0320cd
merge main
ChavdaSachin Mar 9, 2026
b0982e0
Add support for other expense types
ChavdaSachin Mar 11, 2026
844e2a2
Pass default values to avoid form errors
ChavdaSachin Mar 13, 2026
dfdc376
Fix padding
ChavdaSachin Mar 13, 2026
77eb253
Avoid losing focus input focus when flip button is clicked.
ChavdaSachin Mar 13, 2026
c3f9c11
Pass default values to avoid form errors
ChavdaSachin Mar 13, 2026
08f20bf
FIx scroll behavior on input focus for mobile-safari
ChavdaSachin Mar 13, 2026
1b5dedf
Lint, prettier, type
ChavdaSachin Mar 13, 2026
1000dd8
Merge remote-tracking branch 'upstream/main' into ManualExpense-flow-…
ChavdaSachin Mar 13, 2026
778cf84
lint
ChavdaSachin Mar 13, 2026
2207ae1
Type
ChavdaSachin Mar 13, 2026
3b983c1
Do not render the amount for new flow
ChavdaSachin Mar 13, 2026
fe5d695
undo unwanted changes
ChavdaSachin Mar 13, 2026
3bd7593
add condition
ChavdaSachin Mar 13, 2026
15d19e7
prettier
ChavdaSachin Mar 13, 2026
88d28ca
update the test for new flow
ChavdaSachin Mar 13, 2026
ce4c5e9
DIsable inline amount input for timeRequests
ChavdaSachin Mar 13, 2026
1957076
Merge main
ChavdaSachin Mar 16, 2026
9018845
Refine currency preservation logic
ChavdaSachin Mar 17, 2026
2d2cecf
Merge main
ChavdaSachin Mar 17, 2026
0b93ee8
FIx TS
ChavdaSachin Mar 17, 2026
3539ec7
DIsable amount field for time requests and distance requests.
ChavdaSachin Mar 18, 2026
12cf938
UI refinement: Use pushRow instead of inline input when field is disa…
ChavdaSachin Mar 18, 2026
72bb871
prettier
ChavdaSachin Mar 18, 2026
975a041
Pass correct currency code for each calculation.
ChavdaSachin Mar 18, 2026
8a6603a
Refactor manual expense flow: remove beta overrides and streamline am…
ChavdaSachin Mar 23, 2026
37e956f
cleanup
ChavdaSachin Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ const CONST = {
PERSONAL_CARD_IMPORT: 'personalCardImport',
SUGGESTED_FOLLOWUPS: 'suggestedFollowups',
FREEZE_CARD: 'freezeCard',
NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down
5 changes: 5 additions & 0 deletions src/components/AmountTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ type AmountTextInputProps = {

/** A unique identifier for this text input for testing purposes */
testID?: string;

/** Component to render on the right hand side of the input - only shown if clear button is not rendered */
rightHandSideComponent?: React.ReactNode;
} & Pick<BaseTextInputProps, 'autoFocus' | 'autoGrowExtraSpace' | 'submitBehavior' | 'ref' | 'onFocus' | 'onBlur' | 'disabled' | 'accessibilityLabel'>;

function AmountTextInput({
Expand All @@ -64,6 +67,7 @@ function AmountTextInput({
ref,
disabled,
accessibilityLabel,
rightHandSideComponent,
...rest
}: AmountTextInputProps) {
const navigation = useNavigation();
Expand Down Expand Up @@ -99,6 +103,7 @@ function AmountTextInput({
disableKeyboardShortcuts
shouldUseFullInputHeight
shouldApplyPaddingToContainer={shouldApplyPaddingToContainer}
rightHandSideComponent={rightHandSideComponent}
navigation={navigation}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
Expand Down
65 changes: 54 additions & 11 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ import EducationalTooltip from './Tooltip/EducationalTooltip';

type MoneyRequestConfirmationListProps = {
/** Callback to inform parent modal of success */
onConfirm?: (selectedParticipants: Participant[]) => void;
onConfirm?: (selectedParticipants: Participant[], amount?: number, currency?: string) => void;

/** Callback to parent modal to pay someone */
onSendMoney?: (paymentMethod: PaymentMethodType | undefined) => void;
Expand Down Expand Up @@ -290,6 +290,7 @@ function MoneyRequestConfirmationList({
const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES);
const {getCurrencySymbol, getCurrencyDecimals} = useCurrencyListActions();
const {isBetaEnabled} = usePermissions();
const isNewManualExpenseFlowEnabled = isBetaEnabled(CONST.BETAS.NEW_MANUAL_EXPENSE_FLOW);
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();

Expand Down Expand Up @@ -386,10 +387,17 @@ function MoneyRequestConfirmationList({

const distance = getDistanceInMeters(transaction, unit);
const prevDistance = usePrevious(distance);
// For the new manual expense flow (beta), track the latest amount/currency the user has typed
// using refs rather than state to avoid re-rendering 20+ Onyx-watching components on every
// keystroke. The refs are only read once on final submission to persist the value to Onyx.
const pendingAmountRef = useRef<number | null>(iouAmount);
const pendingCurrencyRef = useRef<string>(iouCurrencyCode ?? CONST.CURRENCY.USD);

const shouldCalculateDistanceAmount = isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency || prevUnit !== unit);
const shouldCalculateDistanceAmount =
!pendingAmountRef.current && isDistanceRequest && (iouAmount === 0 || prevRate !== rate || prevDistance !== distance || prevCurrency !== currency || prevUnit !== unit);

const shouldCalculatePerDiemAmount = isPerDiemRequest && (iouAmount === 0 || JSON.stringify(prevSubRates) !== JSON.stringify(subRates) || prevCurrency !== currency);
const shouldCalculatePerDiemAmount =
!pendingAmountRef.current && isPerDiemRequest && (iouAmount === 0 || JSON.stringify(prevSubRates) !== JSON.stringify(subRates) || prevCurrency !== currency);

const hasRoute = hasRouteUtil(transaction, isDistanceRequest);
const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate) && !isMovingTransactionFromTrackExpense;
Expand All @@ -400,9 +408,11 @@ function MoneyRequestConfirmationList({

if (shouldCalculateDistanceAmount) {
amountToBeUsed = distanceRequestAmount;
pendingCurrencyRef.current = currency;
} else if (shouldCalculatePerDiemAmount) {
const perDiemRequestAmount = computePerDiemExpenseAmount({subRates});
amountToBeUsed = perDiemRequestAmount;
pendingCurrencyRef.current = currency;
}

let formattedAmount = convertToDisplayString(amountToBeUsed, isDistanceRequest ? currency : iouCurrencyCode);
Expand All @@ -422,6 +432,14 @@ function MoneyRequestConfirmationList({
const [didConfirmSplit, setDidConfirmSplit] = useState(false);
const [showMoreFields, setShowMoreFields] = useState(false);

// Callbacks passed to the footer to capture the user's edits without triggering re-renders
const handleAmountChange = useCallback((value: number | null) => {
pendingAmountRef.current = value;
}, []);
const handleCurrencyChange = useCallback((value: string) => {
pendingCurrencyRef.current = value;
}, []);

useEffect(() => {
setShowMoreFields(false);
}, [transactionID]);
Expand Down Expand Up @@ -604,20 +622,22 @@ function MoneyRequestConfirmationList({
}
} else if (isTypeTrackExpense) {
text = translate('iou.createExpense');
if (iouAmount !== 0) {
if (iouAmount !== 0 && !isNewManualExpenseFlowEnabled) {
text = translate('iou.createExpenseWithAmount', {amount: formattedAmount});
}
} else if (isTypeSplit && iouAmount === 0) {
text = translate('iou.splitExpense');
} else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute || isPerDiemRequest) {
text = translate('iou.createExpense');
if (iouAmount !== 0) {
if (iouAmount !== 0 && !isNewManualExpenseFlowEnabled) {
text = translate('iou.createExpenseWithAmount', {amount: formattedAmount});
}
} else if (isTypeSplit) {
text = translate('iou.splitAmount', formattedAmount);
} else if (iouAmount === 0) {
text = translate('iou.createExpense');
} else if (isNewManualExpenseFlowEnabled) {
text = translate('iou.createExpense');
} else {
text = translate('iou.createExpenseWithAmount', {amount: formattedAmount});
}
Expand All @@ -641,6 +661,7 @@ function MoneyRequestConfirmationList({
policy,
translate,
formattedAmount,
isNewManualExpenseFlowEnabled,
]);

const onSplitShareChange = useCallback(
Expand Down Expand Up @@ -1000,6 +1021,15 @@ function MoneyRequestConfirmationList({
setFormError('iou.error.noParticipantSelected');
return;
}

const amountForValidation = isNewManualExpenseFlowEnabled ? pendingAmountRef.current : iouAmount;
const isAmountMissingForManualFlow = amountForValidation === null || amountForValidation === undefined;

if (iouType !== CONST.IOU.TYPE.PAY && isNewManualExpenseFlowEnabled && isAmountMissingForManualFlow) {
setFormError('common.error.invalidAmount');
return;
}

if (!isEditingSplitBill && isMerchantRequired && (isMerchantEmpty || (shouldDisplayFieldError && isMerchantMissing(transaction)))) {
setFormError('iou.error.invalidMerchant');
return;
Expand Down Expand Up @@ -1054,17 +1084,21 @@ function MoneyRequestConfirmationList({
if (iouType !== CONST.IOU.TYPE.PAY) {
// validate the amount for distance expenses
const decimals = getCurrencyDecimals(iouCurrencyCode);
if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !validateAmount(String(iouAmount), decimals, CONST.IOU.DISTANCE_REQUEST_AMOUNT_MAX_LENGTH)) {
if (
isDistanceRequest &&
!isDistanceRequestWithPendingRoute &&
!validateAmount(String(isNewManualExpenseFlowEnabled ? pendingAmountRef.current : iouAmount), decimals, CONST.IOU.DISTANCE_REQUEST_AMOUNT_MAX_LENGTH)
) {
setFormError('common.error.invalidAmount');
return;
}

if (isDistanceRequest && Math.abs(iouAmount) > CONST.IOU.MAX_SAFE_AMOUNT) {
if (isDistanceRequest && Math.abs(isNewManualExpenseFlowEnabled ? (pendingAmountRef.current ?? 0) : iouAmount) > CONST.IOU.MAX_SAFE_AMOUNT) {
setFormError('iou.error.distanceAmountTooLarge');
return;
}

if (isTimeRequest && !isValidTimeExpenseAmount(iouAmount, iouCurrencyCode, decimals)) {
if (isTimeRequest && !isValidTimeExpenseAmount(isNewManualExpenseFlowEnabled ? (pendingAmountRef.current ?? 0) : iouAmount, iouCurrencyCode, decimals)) {
setFormError('iou.timeTracking.amountTooLargeError');
return;
}
Expand All @@ -1091,7 +1125,12 @@ function MoneyRequestConfirmationList({
return;
}

onConfirm?.(selectedParticipants);
// For the new manual expense flow (beta) the user edits amount/currency inline.
// Pass the pending values directly through the callback so the caller can use
// them immediately — bypassing the Onyx → React state propagation delay that
// would otherwise cause requestMoney to read the stale draft value.
// Non-beta flows leave the refs as null, so the caller falls back to item.amount.
onConfirm?.(selectedParticipants, pendingAmountRef.current ?? undefined, pendingCurrencyRef.current ?? undefined);
} else {
if (!paymentMethod) {
return;
Expand Down Expand Up @@ -1127,6 +1166,7 @@ function MoneyRequestConfirmationList({
iouCurrencyCode,
isDistanceRequest,
isDistanceRequestWithPendingRoute,
isNewManualExpenseFlowEnabled,
iouAmount,
formError,
onConfirm,
Expand Down Expand Up @@ -1258,7 +1298,9 @@ function MoneyRequestConfirmationList({
confirm,
iouCurrencyCode,
policyID,
reportID,
isConfirmed,
isConfirming,
splitOrRequestOptions,
errorMessage,
expensesNumber,
Expand All @@ -1270,8 +1312,6 @@ function MoneyRequestConfirmationList({
styles.productTrainingTooltipWrapper,
shouldShowProductTrainingTooltip,
renderProductTrainingTooltip,
isConfirming,
reportID,
]);

const isCompactMode = useMemo(() => !showMoreFields && isScanRequest, [isScanRequest, showMoreFields]);
Expand All @@ -1291,6 +1331,7 @@ function MoneyRequestConfirmationList({
currency={currency}
didConfirm={!!didConfirm}
distance={distance}
amount={amountToBeUsed}
formattedAmount={formattedAmount}
formattedAmountPerAttendee={formattedAmountPerAttendee}
formError={formError}
Expand Down Expand Up @@ -1346,6 +1387,8 @@ function MoneyRequestConfirmationList({
isDescriptionRequired={isDescriptionRequired}
showMoreFields={showMoreFields}
setShowMoreFields={setShowMoreFields}
onAmountChange={handleAmountChange}
onCurrencyChange={handleCurrencyChange}
/>
</View>
);
Expand Down
Loading
Loading