Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
135caf9
Add commuter exclusion types, constants, and strings
fedirjh Jun 17, 2026
8324df2
Render commuter exclusion breakdown on confirmation, view, and edit
fedirjh Jun 17, 2026
4456e9a
Gate manual/odometer entry on commuter exclusion policies
fedirjh Jun 17, 2026
cdafc1b
Route COMMUTER_EXCLUSION system action through ActionContentRouter
fedirjh Jun 17, 2026
44f5bd1
Optimistic commuter exclusion on createDistanceRequest
fedirjh Jun 17, 2026
932c326
Block manual and odometer entry on commuter-exclusion workspaces
fedirjh Jun 17, 2026
6bdf910
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 17, 2026
8974878
Merge branch 'alberto-createExclusion' into commuter-exclusions-app
fedirjh Jun 18, 2026
ee0cba7
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 18, 2026
290daee
Consolidate commuter exclusion constants under CONST.POLICY
fedirjh Jun 18, 2026
2f9b65a
Add commuter exclusion breakdown and unit label utilities
fedirjh Jun 18, 2026
bbe4aa3
Display commuter exclusion breakdown in the distance field
fedirjh Jun 18, 2026
c41d074
Apply reimbursable amount and seed optimistic commuter exclusion data
fedirjh Jun 18, 2026
103dcd5
Render commuter exclusion system message as HTML with settings link
fedirjh Jun 18, 2026
a366047
Remove buildOptimisticCommuterExclusionReportAction function and its …
fedirjh Jun 18, 2026
2833278
fix: always use computed distanceRequestAmount for distance requests
fedirjh Jun 18, 2026
7bc17b8
fix: pass commuterExclusionBreakdown from hook to confirmation UI
fedirjh Jun 18, 2026
fbee94b
fix: show reimbursable distance in merchant text with commuter exclusion
fedirjh Jun 18, 2026
ad78962
fix: compute commuter exclusion from policy in MoneyRequestView
fedirjh Jun 18, 2026
a01fa2e
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 22, 2026
e45fffd
Use Onyx selector for commuter exclusion tab check, exclude personal …
fedirjh Jun 22, 2026
9d80222
Centralize commuter exclusion distance display in getDistanceForDisplay
fedirjh Jun 22, 2026
8b64f38
Align DistanceField commuter exclusion display with MoneyRequestView …
fedirjh Jun 22, 2026
76504c7
Use shared CommuterExclusionData type in DistanceRequestController an…
fedirjh Jun 22, 2026
1d2c6f6
Add getFormattedDistanceInUnits util for localized commuter exclusion…
fedirjh Jun 22, 2026
e831728
Reuse getFormattedDistanceInUnits for commuter distance labels
fedirjh Jun 22, 2026
61e6047
Refactor commuter exclusion handling in MoneyRequestConfirmationList
fedirjh Jun 22, 2026
057386e
Rename getCommuterExclusionBreakdown to getCommuterExclusionData
fedirjh Jun 22, 2026
e2b751b
Pass commuterExclusionData to getDistanceMerchant
fedirjh Jun 22, 2026
1dabe25
Break MoneyRequestUtils/DistanceRequestUtils dependency cycle
fedirjh Jun 22, 2026
d13d5c6
Show map/GPS-required confirm modal for manual and odometer distance
fedirjh Jun 22, 2026
b3c370c
Refactor commuter exclusion distance formatting in multiple languages
fedirjh Jun 22, 2026
27bc11b
Format commuter exclusion change-log messages via getFormattedDistanc…
fedirjh Jun 22, 2026
e6aa09d
Add mapOrGpsDistanceRequired translations for all locales
fedirjh Jun 22, 2026
c225c32
Add commuter exclusion translations for multiple languages
fedirjh Jun 22, 2026
caa7327
Add mock implementations for distance request utilities in tests
fedirjh Jun 22, 2026
c80f6da
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jun 22, 2026
814c91b
Scope distance amount recalculation to commuter exclusions via should…
fedirjh Jun 22, 2026
b13da76
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 29, 2026
dd83c0f
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 29, 2026
1a4b275
Add 'commuter' translations for multiple languages
fedirjh Jun 29, 2026
0b8f004
Fix commuter exclusion distance display
fedirjh Jun 29, 2026
332ffb6
Use shared distance merchant formatting
fedirjh Jun 29, 2026
eddff4c
Add commuter fields to debug validation
fedirjh Jun 29, 2026
2a633bf
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jun 29, 2026
67e2da5
Refactor fixed distance unit style
fedirjh Jun 29, 2026
39a7adb
Extract distance display formatting
fedirjh Jun 29, 2026
956288a
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jun 29, 2026
5d25316
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jun 29, 2026
832893e
Cover commuter distance merchant updates
fedirjh Jun 29, 2026
7468b01
Fix ESLint type-assertion errors in commuter exclusion messages
fedirjh Jun 29, 2026
f678ffc
Add HouseWithMap illustration SVG and update imports
fedirjh Jun 29, 2026
9d42637
Update IOU request steps to use HouseWithMap illustration and add new…
fedirjh Jun 30, 2026
1d7bc19
Refactor HouseWithMap SVG illustration by removing unnecessary metada…
fedirjh Jun 30, 2026
44583be
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jun 30, 2026
7a565b0
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jun 30, 2026
6c4c521
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 6, 2026
241934e
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 6, 2026
4a93ae8
refactor: reorganize imports across multiple files for consistency an…
fedirjh Jul 6, 2026
1423373
refactor: remove DistanceWithCommuterExclusion component and update r…
fedirjh Jul 6, 2026
4588797
refactor: extract getDistanceDisplayDetailsWithCommuter to DRY commut…
fedirjh Jul 6, 2026
8d8f67a
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jul 6, 2026
fc675a8
refactor: extract useCommuterExclusionGuard hook to deduplicate commu…
fedirjh Jul 6, 2026
da3aa04
chore: add justification for eslint-disable on deprecated Localize.tr…
fedirjh Jul 6, 2026
8e19689
refactor: move commuter exclusion stored check inside getCommuterExcl…
fedirjh Jul 6, 2026
e671fd4
refactor: update import paths in useCommuterExclusionGuard hook
fedirjh Jul 6, 2026
5474b61
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 6, 2026
cf90db5
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 9, 2026
0b3f398
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 9, 2026
73ac23f
Refactor ConfirmContent component to reposition dismiss icon rendering
fedirjh Jul 9, 2026
16a95ab
Guard commuter exclusions during workspace changes
fedirjh Jul 9, 2026
9f368a0
Block workspace changes for commuter exclusions
fedirjh Jul 9, 2026
c67fb8b
Store commuter exclusions on distance requests
fedirjh Jul 9, 2026
c56d0d0
Cover commuter exclusion distance updates
fedirjh Jul 9, 2026
ea20193
Clean up commuter exclusion distance display
fedirjh Jul 9, 2026
7a0eddc
Tighten commuter exclusion validation
fedirjh Jul 9, 2026
a0a28d8
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 9, 2026
1ea13ba
Update distance display tests
fedirjh Jul 9, 2026
c6b1ef5
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jul 9, 2026
b43208b
Merge branch 'main' into commuter-exclusions-app
fedirjh Jul 10, 2026
ba9b360
fedirjh Jul 13, 2026
e75c44f
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 13, 2026
4517c98
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 14, 2026
09c12ee
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 14, 2026
393ac24
Exclude inactive workspaces from commuter exclusion checks
fedirjh Jul 14, 2026
c2ec657
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jul 14, 2026
6d07f8e
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 14, 2026
6fb0277
Merge branch 'Expensify:main' into commuter-exclusions-app
fedirjh Jul 14, 2026
4651d1f
Merge remote-tracking branch 'upstream' into commuter-exclusions-app
fedirjh Jul 14, 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,7 @@ const CONST = {
HOLD_COMMENT: 'HOLDCOMMENT',
INTEGRATION_SYNC_FAILED: 'INTEGRATIONSYNCFAILED',
COMPANY_CARD_CONNECTION_BROKEN: 'COMPANYCARDCONNECTIONBROKEN',
COMMUTER_EXCLUSION: 'COMMUTEREXCLUSION',
PLAID_BALANCE_FAILURE: 'PLAIDBALANCEFAILURE',
IOU: 'IOU',
INTEGRATIONS_MESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/chunks/illustrations.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ import HeadSet from '@assets/images/simple-illustrations/simple-illustration__he
import Hourglass from '@assets/images/simple-illustrations/simple-illustration__hourglass.svg';
import HouseMoney from '@assets/images/simple-illustrations/simple-illustration__house-money.svg';
import House from '@assets/images/simple-illustrations/simple-illustration__house.svg';
import HouseWithMap from '@assets/images/simple-illustrations/simple-illustration__housewithmap.svg';
import InvoiceBlue from '@assets/images/simple-illustrations/simple-illustration__invoice.svg';
import LifeRing from '@assets/images/simple-illustrations/simple-illustration__lifering.svg';
import Lightbulb from '@assets/images/simple-illustrations/simple-illustration__lightbulb.svg';
Expand Down Expand Up @@ -410,6 +411,7 @@ const Illustrations = {
HeadSet,
Hourglass,
House,
HouseWithMap,
HouseMoney,
LifeRing,
Lightbulb,
Expand Down
14 changes: 13 additions & 1 deletion src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import useAttendees from '@hooks/useAttendees';
import useCommuterExclusionGuard from '@hooks/useCommuterExclusionGuard';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsInLandscapeMode from '@hooks/useIsInLandscapeMode';
import useLocalize from '@hooks/useLocalize';
Expand Down Expand Up @@ -254,6 +255,11 @@ function MoneyRequestConfirmationList({
const isDistanceRequest = isDistanceRequestUtil(transaction);
const isManualDistanceRequest = isManualDistanceRequestUtil(transaction);
const isGPSDistanceRequest = isGPSDistanceRequestUtil(transaction);
const shouldBlockManualOrOdometerDistanceRequest = useCommuterExclusionGuard({
policyID: policy?.id,
isManualDistanceRequest,
isOdometerDistanceRequest,
});

const iouAmount = hasValidModifiedAmount(transaction) ? Number(transaction?.modifiedAmount) : (transaction?.amount ?? 0);
const iouCurrencyCode = getCurrency(transaction);
Expand Down Expand Up @@ -500,7 +506,12 @@ function MoneyRequestConfirmationList({
setFormError,
setDidConfirmSplit,
showDelegateNoAccessModal,
onConfirm,
onConfirm: () => {
if (shouldBlockManualOrOdometerDistanceRequest()) {
return;
}
onConfirm?.();
},
onSendMoney,
});

Expand Down Expand Up @@ -561,6 +572,7 @@ function MoneyRequestConfirmationList({
expenseDate: getCreated(transaction),
customUnitRateID,
shouldShowRateAutoUpdatedTooltip,
customUnit: transaction?.comment?.customUnit,
}}
amountDisplay={{amount: amountToBeUsed, formattedAmount, formattedAmountPerAttendee}}
requiredFlags={{isCategoryRequired, isMerchantRequired, isDescriptionRequired}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import useOnyx from '@hooks/useOnyx';
import usePersonalPolicy from '@hooks/usePersonalPolicy';
import usePrevious from '@hooks/usePrevious';

import {clearMoneyRequestRateAutoUpdated, setCustomUnitRateID, setMoneyRequestAmount, setMoneyRequestMerchant, setMoneyRequestPendingFields} from '@libs/actions/IOU/MoneyRequest';
import {
clearMoneyRequestRateAutoUpdated,
setCustomUnitRateID,
setMoneyRequestAmount,
setMoneyRequestCommuterExclusionFields,
setMoneyRequestMerchant,
setMoneyRequestPendingFields,
} from '@libs/actions/IOU/MoneyRequest';
import {setSplitShares} from '@libs/actions/IOU/Split';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import type {MileageRate} from '@libs/DistanceRequestUtils';
Expand Down Expand Up @@ -146,10 +153,10 @@ function DistanceRequestController({
if (isReadOnly) {
return;
}
const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0);
setMoneyRequestAmount(transactionID, amount, currency ?? '');
// Use the (commuter-exclusion-aware) reimbursable amount so the seeded amount matches what the backend will calculate.
setMoneyRequestAmount(transactionID, distanceRequestAmount, currency ?? '');
isFirstUpdatedDistanceAmount.current = true;
}, [distance, rate, isReadOnly, unit, transactionID, currency, isDistanceRequest]);
}, [distanceRequestAmount, isReadOnly, transactionID, currency, isDistanceRequest]);

useEffect(() => {
if (!shouldCalculateDistanceAmount || !transactionID || isReadOnly) {
Expand Down Expand Up @@ -239,6 +246,19 @@ function DistanceRequestController({
isManualDistanceRequest,
);
setMoneyRequestMerchant(transactionID, distanceMerchant, true);

setMoneyRequestCommuterExclusionFields({
transactionID,
transaction,
policy,
customUnitRateID,
routeDistanceMeters: distance,
distanceUnit: unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES,
translate,
toLocaleDigit,
getCurrencySymbol,
personalPolicyOutputCurrency: personalPolicy?.outputCurrency,
});
}, [
isDistanceRequestWithPendingRoute,
hasRoute,
Expand All @@ -254,6 +274,9 @@ function DistanceRequestController({
isReadOnly,
getCurrencySymbol,
isManualDistanceRequest,
policy,
customUnitRateID,
personalPolicy?.outputCurrency,
]);

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@ function useConfirmationValidation({
return {errorKey: 'iou.error.genericSmartscanFailureMessage', shouldSetDidConfirmSplit: true};
}

if (isEditingSplitBill && iouAmount === 0) {
const isFullyCoveredByCommuterExclusion =
isDistanceRequest && (transaction?.comment?.customUnit?.commuterExclusion ?? 0) > 0 && transaction?.comment?.customUnit?.reimbursableDistance === 0;
if (isEditingSplitBill && iouAmount === 0 && !isFullyCoveredByCommuterExclusion) {
return {errorKey: 'iou.error.invalidAmount'};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import CONST from '@src/CONST';
import type {IOUAction, IOUType} from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Unit} from '@src/types/onyx/Policy';
import type {TransactionCustomUnit} from '@src/types/onyx/Transaction';

import React from 'react';

type DistanceFieldProps = {
hasRoute: boolean;
distance: number;
unit: Unit | undefined;
rate: number | undefined;
isManualDistanceRequest: boolean;
isOdometerDistanceRequest: boolean;
isGPSDistanceRequest: boolean;
Expand All @@ -28,13 +28,13 @@ type DistanceFieldProps = {
iouType: Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND>;
reportID: string;
reportActionID: string | undefined;
customUnit?: TransactionCustomUnit;
};

function DistanceField({
hasRoute,
distance,
unit,
rate,
isManualDistanceRequest,
isOdometerDistanceRequest,
isGPSDistanceRequest,
Expand All @@ -45,15 +45,22 @@ function DistanceField({
iouType,
reportID,
reportActionID,
customUnit,
}: DistanceFieldProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const displayUnit = unit ?? customUnit?.distanceUnit;
const commuterExclusionData = DistanceRequestUtils.getCommuterExclusionDisplayData(customUnit, displayUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES);
const displayTitle = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, translate, true, isManualDistanceRequest, commuterExclusionData);
const {distanceToDisplayDescription, distanceToDisplayHintText} = DistanceRequestUtils.getDistanceDisplayDetailsWithCommuter(commuterExclusionData, displayUnit, translate);

return (
<MenuItemWithTopDescription
shouldShowRightIcon={!isReadOnly && !isGPSDistanceRequest}
title={DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate, undefined, isManualDistanceRequest)}
description={translate('common.distance')}
title={displayTitle}
description={distanceToDisplayDescription}
hintText={distanceToDisplayHintText}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {MileageRate} from '@libs/DistanceRequestUtils';

import type {TranslationPaths} from '@src/languages/types';
import type {Unit} from '@src/types/onyx/Policy';
import type {TransactionCustomUnit} from '@src/types/onyx/Transaction';

/** Pre-formatted amount values displayed in Amount/Attendee fields */
type AmountDisplay = {
Expand All @@ -22,6 +23,7 @@ type DistanceData = {
expenseDate: string | undefined;
customUnitRateID: string | undefined;
shouldShowRateAutoUpdatedTooltip?: boolean;
customUnit?: TransactionCustomUnit;
};

/** Distance-mode discriminators (manual / odometer / GPS) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ function TransactionDetailsFields({
hasRoute={distanceData.hasRoute}
distance={distanceData.distance}
unit={distanceData.unit}
rate={distanceData.rate}
isManualDistanceRequest={isManualDistanceRequest}
isOdometerDistanceRequest={isOdometerDistanceRequest}
isGPSDistanceRequest={isGPSDistanceRequest}
Expand All @@ -154,6 +153,7 @@ function TransactionDetailsFields({
iouType={iouType}
reportID={reportID}
reportActionID={reportActionID}
customUnit={distanceData.customUnit}
/>
)}

Expand Down
10 changes: 8 additions & 2 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,12 @@ function MoneyRequestView({
getCurrencySymbol,
isOffline,
);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate, undefined, isManualDistanceRequest);

const distanceUnitValue = transaction?.comment?.customUnit?.distanceUnit ?? unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES;
const commuterExclusionData = DistanceRequestUtils.getCommuterExclusionDisplayData(transaction?.comment?.customUnit, distanceUnitValue);
const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, translate, true, isManualDistanceRequest, commuterExclusionData);
const {distanceToDisplayDescription, distanceToDisplayHintText} = DistanceRequestUtils.getDistanceDisplayDetailsWithCommuter(commuterExclusionData, distanceUnitValue, translate);

let merchantTitle = isEmptyMerchant ? '' : transactionMerchant;
let amountTitle = formattedTransactionAmount?.toString() || '';
if (isTransactionScanning) {
Expand Down Expand Up @@ -936,8 +941,9 @@ function MoneyRequestView({
<>
<OfflineWithFeedback pendingAction={getPendingFieldAction('waypoints') ?? getPendingFieldAction('merchant')}>
<MenuItemWithTopDescription
description={translate('common.distance')}
description={distanceToDisplayDescription}
title={distanceToDisplay}
hintText={distanceToDisplayHintText}
numberOfLinesTitle={2}
interactive={canEditDistance}
shouldShowRightIcon={canEditDistance}
Expand Down
Loading
Loading