Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
526afbe
Fix distance preview mismatch with expense details header
nabi-ebrahimi Sep 14, 2026
b585e18
Sync distance preview accessibility amount with visible display
nabi-ebrahimi Sep 14, 2026
fbf832d
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Sep 21, 2026
205e28a
Deduplicate distance preview normalization and add regression tests
nabi-ebrahimi Sep 21, 2026
ea71170
Extract shared distance display transaction hook
nabi-ebrahimi Sep 21, 2026
bd2f262
Fix display transaction hook imports and report context handling
nabi-ebrahimi Sep 21, 2026
33ca266
Align distance preview policy context and add regression coverage
nabi-ebrahimi Sep 21, 2026
f1afddd
Align distance preview policy detection and strengthen regression tests
nabi-ebrahimi Sep 21, 2026
db787f9
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Sep 23, 2026
ae9a578
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Sep 25, 2026
66d10f4
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 5, 2026
2e3ea23
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 6, 2026
69dbb37
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 7, 2026
083ddb2
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 9, 2026
89ff570
Preserve historical rates and manual amounts in self-DM distance disp…
nabi-ebrahimi Oct 10, 2026
898418a
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 12, 2026
19b6623
Fix nested ternary lint error in distance transaction display
nabi-ebrahimi Oct 12, 2026
cae6c9b
Revert historical rate and manual amount handling for distance displays
nabi-ebrahimi Oct 12, 2026
0863c35
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 12, 2026
47a7be4
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 13, 2026
e638712
Fix distance expense previews to preserve saved rates
nabi-ebrahimi Oct 13, 2026
1754bc3
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 14, 2026
599a060
Add coverage for matching currency symbols in distance previews
nabi-ebrahimi Oct 14, 2026
431e5a2
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 15, 2026
85a23da
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 16, 2026
529645f
Clarify distance rate restoration logic
nabi-ebrahimi Oct 16, 2026
2b82f09
Ensure saved distance restoration flag is boolean
nabi-ebrahimi Oct 16, 2026
9da497e
Restore all-betas permission check
nabi-ebrahimi Oct 16, 2026
b51f015
Fix optimistic Self DM distance rate calculation
nabi-ebrahimi Oct 16, 2026
1e80e8d
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 16, 2026
44204ea
`Preserve full distance amount across offline rate changes`
nabi-ebrahimi Oct 17, 2026
5e75ad4
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 17, 2026
d4a4113
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 19, 2026
999ceec
Fix distance expense amount changing when switching languages
nabi-ebrahimi Oct 19, 2026
0706971
Preserve saved distance rates when currency data loads
nabi-ebrahimi Oct 19, 2026
ec0c250
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 20, 2026
a8f39bd
Extract distance merchant parsing and add formatter round-trip tests
nabi-ebrahimi Oct 20, 2026
6d1a5d5
Document policy parameters in useDisplayTransaction
nabi-ebrahimi Oct 20, 2026
06d7c5c
Add explicit return type to useDisplayTransaction
nabi-ebrahimi Oct 20, 2026
55aac36
Use a named function for restoreSavedReducedValues
nabi-ebrahimi Oct 20, 2026
2612b53
Share distance merchant prefix formatting
nabi-ebrahimi Oct 20, 2026
597b8d5
Cover cross-policy rate lookup and Self DM route edits
nabi-ebrahimi Oct 20, 2026
e294528
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 20, 2026
c923cda
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 22, 2026
7ec6bd9
Preserve historical distance amounts after workspace rate changes
nabi-ebrahimi Oct 22, 2026
7dc7c34
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 23, 2026
d1dc6de
Fix commuter-excluded amounts when moving distance expenses back to r…
nabi-ebrahimi Oct 23, 2026
7113e34
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 26, 2026
af40e39
Merge branch 'main' into fix-distance-preview-header-mismatch-100188
nabi-ebrahimi Oct 26, 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
24 changes: 6 additions & 18 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import useConfirmModal from '@hooks/useConfirmModal';
import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDelegateAccountID from '@hooks/useDelegateAccountID';
import useDistanceRateOriginalPolicy from '@hooks/useDistanceRateOriginalPolicy';
import useDisplayTransaction from '@hooks/useDisplayTransaction';
import useEnvironment from '@hooks/useEnvironment';
import useHasMultipleSplitChildren from '@hooks/useHasMultipleSplitChildren';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
Expand Down Expand Up @@ -66,7 +66,6 @@ import {
canShowReimbursableToggleForPolicy,
canSubmitPerDiemExpenseFromWorkspace,
findVendorByID,
getDistanceRateCustomUnitRate,
getLengthOfTag,
getPerDiemCustomUnit,
getPolicyByCustomUnitID,
Expand Down Expand Up @@ -106,7 +105,6 @@ import {
getCurrency,
getDescription,
getDetailedExpenseTypeTranslationKey,
getDisplayTransactionWithoutInvalidCommuterExclusion,
getDistanceInMeters,
getFormattedCreated,
getOriginalAmountForDisplay,
Expand Down Expand Up @@ -253,9 +251,9 @@ function MoneyRequestView({
const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction);
const perDiemOriginalPolicy = getPolicyByCustomUnitID(transaction, policiesWithPerDiem);

const customUnitRateID = isDistanceRequestTransactionUtils(transaction) ? transaction?.comment?.customUnit?.customUnitRateID : undefined;
const shouldLookupDistancePolicy = !!customUnitRateID && !getDistanceRateCustomUnitRate(expensePolicy, customUnitRateID);
const distanceOriginalPolicy = useDistanceRateOriginalPolicy(customUnitRateID, shouldLookupDistancePolicy);
const [reportPolicyType] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${parentReport?.policyID}`, {selector: policyTypeSelector});
// Collect/Control (group) policies are always tied to an expense chat, so a group policy type means this is a policy expense chat.
const isPolicyExpenseChat = isGroupPolicyByType(reportPolicyType);

let policy;
let policyID;
Expand All @@ -272,6 +270,8 @@ function MoneyRequestView({
policyID = parentReport?.policyID;
}

const {displayTransaction, distanceOriginalPolicy} = useDisplayTransaction(transaction, isPolicyExpenseChat, policy, expensePolicy);

// Use the report's real policy, not `policy` above (swapped to an unrelated workspace for
// unreported expenses), else self-DM split editing wrongly redirects to RESTRICTED_ACTION.
const restrictedActionPolicyID = useRestrictedActionPolicyID(expensePolicy);
Expand Down Expand Up @@ -303,10 +303,6 @@ function MoneyRequestView({
const isApproved = isReportApproved({report: moneyRequestReport});
const isInvoice = isInvoiceReport(moneyRequestReport);
const isTrackExpense = !mergeTransactionID && isTrackExpenseReportNew(transactionThreadReport, moneyRequestReport, parentReportAction);
const [reportPolicyType] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`, {selector: policyTypeSelector});
// Collect/Control (group) policies are always tied to an expense chat, so a group policy type means this is a policy expense chat.
const isPolicyExpenseChat = isGroupPolicyByType(reportPolicyType);

let iouType: ValueOf<typeof CONST.IOU.TYPE>;
if (isTrackExpense) {
iouType = CONST.IOU.TYPE.TRACK;
Expand All @@ -317,14 +313,6 @@ function MoneyRequestView({
}

const allowNegativeAmount = shouldEnableNegative(parentReport, policy, iouType);
const displayTransaction = getDisplayTransactionWithoutInvalidCommuterExclusion({
transaction,
isPolicyExpenseChat,
policy: distanceOriginalPolicy ?? policy,
translate,
getCurrencySymbol,
});

const {
created: transactionDate,
amount: transactionAmount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function TransactionPreviewContent({
report,
policy,
transaction,
displayTransaction,
violations,
transactionRawAmount,
offlineWithFeedbackOnClose,
Expand All @@ -86,8 +87,8 @@ function TransactionPreviewContent({
const {environmentURL} = useEnvironment();
const isParentPolicyExpenseChat = isPolicyExpenseChat(chatReport);
const transactionDetails = useMemo<Partial<TransactionDetails>>(
() => getTransactionDetails(transaction, undefined, policy, isParentPolicyExpenseChat) ?? {},
[transaction, policy, isParentPolicyExpenseChat],
() => getTransactionDetails(displayTransaction, undefined, policy, isParentPolicyExpenseChat) ?? {},
Comment thread
nabi-ebrahimi marked this conversation as resolved.
[displayTransaction, policy, isParentPolicyExpenseChat],
);
const {amount, comment: requestComment, merchant, category, currency: requestCurrency} = transactionDetails;
const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`);
Expand Down
13 changes: 9 additions & 4 deletions src/components/ReportActionItem/TransactionPreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed
import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext';

import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useDisplayTransaction from '@hooks/useDisplayTransaction';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useTransactionViolations from '@hooks/useTransactionViolations';
Expand All @@ -11,6 +12,7 @@ import ControlSelection from '@libs/ControlSelection';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import Navigation from '@libs/Navigation/Navigation';
import {isGroupPolicyByType} from '@libs/PolicyUtils';
import {getOriginalMessage, isMoneyRequestAction as isMoneyRequestActionReportActionsUtils} from '@libs/ReportActionsUtils';
import {getTransactionDetails} from '@libs/ReportUtils';
import {getReviewNavigationRoute} from '@libs/TransactionPreviewUtils';
Expand Down Expand Up @@ -67,7 +69,10 @@ function TransactionPreview(props: TransactionPreviewProps) {
const sessionAccountID = session?.accountID;
const areThereDuplicates = allDuplicateIDs.length > 0 && duplicates.length > 0 && allDuplicateIDs.length === duplicates.length;

const transactionDetails = getTransactionDetails(transaction);
const transactionPreview = onPreviewPressed ? transaction : (originalTransaction ?? transaction);
const isParentPolicyExpenseChat = isGroupPolicyByType(policy?.type);
const {displayTransaction} = useDisplayTransaction(transactionPreview, isParentPolicyExpenseChat, policy);
const transactionDetails = getTransactionDetails(displayTransaction);
const {amount: requestAmount, currency: requestCurrency} = transactionDetails ?? {};

const contextMenuReportID = contextAction ? chatReportID : reportID;
Expand All @@ -88,8 +93,6 @@ function TransactionPreview(props: TransactionPreviewProps) {
const navigateToReviewFields = () =>
Navigation.navigate(getReviewNavigationRoute(Navigation.getActiveRoute(), route.params?.reportID, transaction, duplicates, policy, policyCategories, policyTags ?? {}, report));

const transactionPreview = transaction;

const {isBillSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction);

// See description of `transactionRawAmount` prop for more context
Expand Down Expand Up @@ -118,6 +121,7 @@ function TransactionPreview(props: TransactionPreviewProps) {
chatReport={chatReport}
personalDetails={personalDetails}
transaction={transactionPreview}
displayTransaction={displayTransaction}
transactionRawAmount={transactionRawAmount}
report={report}
policy={policy}
Expand All @@ -142,7 +146,8 @@ function TransactionPreview(props: TransactionPreviewProps) {
isBillSplit={isBillSplit}
chatReport={chatReport}
personalDetails={personalDetails}
transaction={originalTransaction ?? transaction}
transaction={transactionPreview}
displayTransaction={displayTransaction}
transactionRawAmount={transactionRawAmount}
report={report}
policy={policy}
Expand Down
3 changes: 3 additions & 0 deletions src/components/ReportActionItem/TransactionPreview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ type TransactionPreviewContentProps = {

transaction: OnyxEntry<Transaction>;

/** Transaction normalized for display-only values. */
displayTransaction: OnyxEntry<Transaction>;

/** The amount of the transaction saved in the database. This is used to deduce who is the sender and who is the receiver of the money request
* In case of Splits the property `transaction` is actually an original transaction (for the whole split) and it does not have the data required to deduce who is the sender */
transactionRawAmount: number;
Expand Down
46 changes: 46 additions & 0 deletions src/hooks/useDisplayTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {getDistanceRateCustomUnitRate} from '@libs/PolicyUtils';
import {getDisplayTransactionWithoutInvalidCommuterExclusion, isDistanceRequest} from '@libs/TransactionUtils';

import type {Policy, Transaction} from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';

import {useCurrencyListActions} from './useCurrencyList';
import useDistanceRateOriginalPolicy from './useDistanceRateOriginalPolicy';
import useLocalize from './useLocalize';

type UseDisplayTransactionResult = {
displayTransaction: OnyxEntry<Transaction>;
distanceOriginalPolicy: OnyxEntry<Policy>;
};

/**
* Normalizes a transaction for display, keeping commuter-reduced values in policy
* expense chats and restoring full-route values elsewhere when possible.
*
* @param policy Fallback policy used for display normalization when no original policy is found.
* @param rateLookupPolicy Policy checked for the selected rate before searching other policies. Defaults to policy.
*/
function useDisplayTransaction(
transaction: OnyxEntry<Transaction>,
isPolicyExpenseChat: boolean,
policy?: OnyxEntry<Policy>,
rateLookupPolicy: OnyxEntry<Policy> = policy,
): UseDisplayTransactionResult {
const {translate} = useLocalize();
const {getCurrencySymbol} = useCurrencyListActions();
const customUnitRateID = isDistanceRequest(transaction) ? transaction?.comment?.customUnit?.customUnitRateID : undefined;
const shouldLookupDistancePolicy = !!customUnitRateID && !getDistanceRateCustomUnitRate(rateLookupPolicy, customUnitRateID);
const distanceOriginalPolicy = useDistanceRateOriginalPolicy(customUnitRateID, shouldLookupDistancePolicy);
Comment thread
nabi-ebrahimi marked this conversation as resolved.
const displayTransaction = getDisplayTransactionWithoutInvalidCommuterExclusion({
transaction,
isPolicyExpenseChat,
policy: distanceOriginalPolicy ?? policy,
translate,
getCurrencySymbol,
});

return {displayTransaction, distanceOriginalPolicy};
}

export default useDisplayTransaction;
42 changes: 24 additions & 18 deletions src/libs/TransactionUtils/getUpdatedTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ function getUpdatedTransaction({
policy = undefined,
policies = undefined,
isSplitTransaction = false,
shouldApplyCommuterExclusion = true,
personalPolicyOutputCurrency,
getCurrencyDecimals,
getCurrencySymbol,
Expand All @@ -146,6 +147,7 @@ function getUpdatedTransaction({
policy?: OnyxEntry<Policy>;
policies?: OnyxCollection<Policy>;
isSplitTransaction?: boolean;
shouldApplyCommuterExclusion?: boolean;
personalPolicyOutputCurrency: string | undefined;
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'];
getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol'];
Expand Down Expand Up @@ -228,15 +230,16 @@ function getUpdatedTransaction({
lodashSet(updatedTransaction, 'comment.customUnit.routeDistanceMeters', distanceInMeters);
}

const commuterExclusionTransactionData = hasAppliedCommuterExclusion(updatedTransaction)
? DistanceRequestUtils.getTransactionCommuterExclusionData({
transaction: updatedTransaction,
policy,
storedCustomUnit: transaction?.comment?.customUnit,
personalPolicyOutputCurrency,
hasTripChanged: waypointsActuallyChanged,
})
: undefined;
const commuterExclusionTransactionData =
shouldApplyCommuterExclusion && hasAppliedCommuterExclusion(updatedTransaction)
Comment thread
nabi-ebrahimi marked this conversation as resolved.
? DistanceRequestUtils.getTransactionCommuterExclusionData({
transaction: updatedTransaction,
policy,
storedCustomUnit: transaction?.comment?.customUnit,
personalPolicyOutputCurrency,
hasTripChanged: waypointsActuallyChanged,
})
: undefined;

if (commuterExclusionTransactionData) {
lodashSet(updatedTransaction, 'comment.customUnit', commuterExclusionTransactionData.customUnit);
Expand Down Expand Up @@ -347,14 +350,15 @@ function getUpdatedTransaction({
// The commuter exclusion is expressed in distance units, so it has to be re-derived against the new
// rate's unit and the converted quantity. The amount and merchant then describe the reimbursable
// distance, matching what the backend stores for modifiedAmount/modifiedMerchant.
const commuterExclusionTransactionData = hasAppliedCommuterExclusion(updatedTransaction)
? DistanceRequestUtils.getTransactionCommuterExclusionData({
transaction: updatedTransaction,
policy,
storedCustomUnit: transaction?.comment?.customUnit,
personalPolicyOutputCurrency,
})
: undefined;
const commuterExclusionTransactionData =
shouldApplyCommuterExclusion && hasAppliedCommuterExclusion(updatedTransaction)
? DistanceRequestUtils.getTransactionCommuterExclusionData({
transaction: updatedTransaction,
policy,
storedCustomUnit: transaction?.comment?.customUnit,
personalPolicyOutputCurrency,
})
: undefined;

if (commuterExclusionTransactionData) {
lodashSet(updatedTransaction, 'comment.customUnit', commuterExclusionTransactionData.customUnit);
Expand All @@ -373,7 +377,9 @@ function getUpdatedTransaction({
DistanceRequestUtils.getCommuterExclusionDisplayData(commuterExclusionTransactionData?.customUnit, unit),
);

updatedTransaction.amount = updatedAmount;
if (!commuterExclusionTransactionData) {
updatedTransaction.amount = updatedAmount;
}
updatedTransaction.modifiedAmount = updatedAmount;
updatedTransaction.modifiedMerchant = updatedMerchant;
updatedTransaction.modifiedCurrency = updatedCurrency;
Expand Down
Loading
Loading