@@ -28,6 +28,7 @@ import {
2828 getClearedPendingFields ,
2929 getMerchant ,
3030 getUpdatedTransaction ,
31+ haveWaypointAddressesChanged ,
3132 isDistanceRequest as isDistanceRequestTransactionUtils ,
3233 isFetchingWaypointsFromServer ,
3334 isOnHold ,
@@ -530,7 +531,10 @@ function updateMoneyRequestDistance({
530531 // Don't sanitize waypoints here - keep all fields for Onyx optimistic data (e.g., keyForList)
531532 // Sanitization happens when building API params
532533 ...( waypoints && { waypoints} ) ,
533- routes,
534+ // Only include routes when the caller explicitly provided them. Including `routes: undefined`
535+ // would make the optimistic merge wipe the existing route, briefly blanking the map thumbnail
536+ // and report preview before the server response restores it.
537+ ...( routes !== undefined && { routes} ) ,
534538 ...( distance && { distance} ) ,
535539 ...( odometerStart !== undefined && { odometerStart} ) ,
536540 ...( odometerEnd !== undefined && { odometerEnd} ) ,
@@ -975,20 +979,46 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
975979 >
976980 > = [ ] ;
977981
978- // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData
979- const pendingFields : OnyxTypes . Transaction [ 'pendingFields' ] = Object . fromEntries ( Object . keys ( transactionChanges ) . map ( ( key ) => [ key , CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ] ) ) ;
982+ // Step 1: Get the transaction being updated
983+ const transaction = getAllTransactions ( ) ?. [ `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transactionID } ` ] ;
984+
985+ // The manual-distance submit path always sends waypoints to keep the BE in sync, even when the user
986+ // only edited the distance number. Detect whether the addresses actually changed so we can skip the
987+ // optimistic side effects (pending field, route clearing, render-path swap to interactive map) that
988+ // would otherwise make the parent map briefly disappear on a pure distance edit.
989+ const hasWaypointAddressesChanged = 'waypoints' in transactionChanges && haveWaypointAddressesChanged ( transaction ?. comment ?. waypoints , transactionChanges . waypoints ) ;
990+ const shouldSuppressWaypointsAsPending = 'waypoints' in transactionChanges && ! hasWaypointAddressesChanged ;
991+
992+ // Step 2: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData
993+ const pendingFields : OnyxTypes . Transaction [ 'pendingFields' ] = Object . fromEntries (
994+ Object . keys ( transactionChanges )
995+ . filter ( ( key ) => ! ( shouldSuppressWaypointsAsPending && key === 'waypoints' ) )
996+ . map ( ( key ) => [ key , CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ] ) ,
997+ ) ;
998+ // Flag `merchant` as pending on any edit that causes the BE to regenerate the receipt
999+ // (waypoints / distance / rate). `merchant` isn't in `transactionChanges`, so the success-data
1000+ // merge won't clear it via `clearedPendingFields` — it persists through the gap between API ack
1001+ // and the Pusher push that delivers the new `receipt.source`. The Pusher push then clears all
1002+ // pendingFields atomically together with the new URL, eliminating the broken-image flash.
1003+ // It also drives the Distance row's offline-feedback strikethrough for pure distance edits.
1004+ if ( 'waypoints' in transactionChanges || 'distance' in transactionChanges || 'customUnitRateID' in transactionChanges ) {
1005+ pendingFields . merchant = CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ;
1006+ }
9801007 const clearedPendingFields = getClearedPendingFields ( transactionChanges ) ;
9811008 const errorFields = Object . fromEntries ( Object . keys ( pendingFields ) . map ( ( key ) => [ key , getMicroSecondOnyxErrorWithTranslationKey ( 'iou.error.genericEditFailureMessage' ) ] ) ) ;
9821009
983- // Step 2: Get all the collections being updated
984- const transaction = getAllTransactions ( ) ?. [ `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transactionID } ` ] ;
985-
9861010 const isTransactionOnHold = isOnHold ( transaction ) ;
9871011 const isFromExpenseReport = isExpenseReport ( iouReport ) || isInvoiceReportReportUtils ( iouReport ) ;
1012+ // Drop the waypoints from the changes fed to getUpdatedTransaction when they didn't actually change,
1013+ // so we skip the waypoints branch that flips isLoading and clobbers amount/merchant before being
1014+ // re-overridden by the distance branch.
1015+ const transactionChangesForOptimisticMerge : TransactionChanges = shouldSuppressWaypointsAsPending
1016+ ? Object . fromEntries ( Object . entries ( transactionChanges ) . filter ( ( [ key ] ) => key !== 'waypoints' ) )
1017+ : transactionChanges ;
9881018 const updatedTransaction : OnyxEntry < OnyxTypes . Transaction > = transaction
9891019 ? getUpdatedTransaction ( {
9901020 transaction,
991- transactionChanges,
1021+ transactionChanges : transactionChangesForOptimisticMerge ,
9921022 isFromExpenseReport,
9931023 isSplitTransaction,
9941024 policy,
@@ -1003,6 +1033,12 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
10031033
10041034 const dataToIncludeInParams : Partial < TransactionDetails > = Object . fromEntries ( Object . entries ( transactionDetails ?? { } ) . filter ( ( [ key ] ) => key in transactionChanges ) ) ;
10051035
1036+ // Preserve the caller's full-precision distance so the server doesn't fire `increasedDistance`
1037+ // when the rounded display value drifts above the exact route distance.
1038+ if ( 'distance' in transactionChanges && typeof transactionChanges . distance === 'number' ) {
1039+ dataToIncludeInParams . distance = transactionChanges . distance ;
1040+ }
1041+
10061042 const apiParams : UpdateMoneyRequestParams = {
10071043 ...dataToIncludeInParams ,
10081044 reportID : iouReport ?. reportID ,
@@ -1017,6 +1053,9 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
10171053 // For split transactions, the merchant and amount are already computed in transactionChanges,
10181054 // so we can build a valid optimistic MODIFIED_EXPENSE even when waypoints are pending.
10191055 const hasSplitDistanceMessageFields = ! ! isSplitTransaction && hasModifiedMerchant && hasModifiedAmount ;
1056+ // When distance is provided alongside waypoints (route was already calculated), we have valid
1057+ // merchant/amount data to build the optimistic report action instead of waiting for the server.
1058+ const hasDistanceWithWaypoints = hasPendingWaypoints && 'distance' in transactionChanges ;
10201059 if ( transaction && updatedTransaction && ( hasPendingWaypoints || hasModifiedDistanceRate ) ) {
10211060 // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors
10221061 successData . push ( {
@@ -1052,7 +1091,11 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
10521091 const updatedReportAction = shouldBuildOptimisticModifiedExpenseReportAction
10531092 ? buildOptimisticModifiedExpenseReportAction ( transactionThreadReport , transaction , transactionChanges , isFromExpenseReport , policy , updatedTransaction , allowNegative )
10541093 : null ;
1055- if ( ( ! hasPendingWaypoints || hasSplitDistanceMessageFields ) && ! ( hasModifiedDistanceRate && isFetchingWaypointsFromServer ( transaction ) ) && updatedReportAction ) {
1094+ if (
1095+ ( ! hasPendingWaypoints || hasSplitDistanceMessageFields || hasDistanceWithWaypoints ) &&
1096+ ! ( hasModifiedDistanceRate && isFetchingWaypointsFromServer ( transaction ) ) &&
1097+ updatedReportAction
1098+ ) {
10561099 apiParams . reportActionID = updatedReportAction . reportActionID ;
10571100
10581101 optimisticData . push ( {
@@ -1264,15 +1307,22 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
12641307 apiParams . attendees = JSON . stringify ( apiParams ?. attendees ) ;
12651308 }
12661309
1267- // Clear out the error fields and loading states on success
1310+ // Clear out the error fields and loading states on success.
1311+ // Only clear `routes` when waypoints/rate changed (the server will push a fresh route via Pusher).
1312+ // For pure distance edits the route is unchanged, and clearing it would make the map briefly disappear.
1313+ // When the caller already supplied a valid optimistic route (waypoint edit with route pre-fetched
1314+ // locally), keep it so the receipt thumbnail and ConfirmedRoute don't flicker between success and
1315+ // the Pusher route push.
1316+ const hasValidOptimisticRoute = ! ! transactionChanges . routes ?. route0 ?. geometry ?. coordinates ?. length ;
1317+ const shouldClearRoutes = ( hasWaypointAddressesChanged || hasModifiedDistanceRate ) && ! hasValidOptimisticRoute ;
12681318 successData . push ( {
12691319 onyxMethod : Onyx . METHOD . MERGE ,
12701320 key : `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transactionID } ` ,
12711321 value : {
12721322 pendingFields : clearedPendingFields ,
12731323 isLoading : false ,
12741324 errorFields : null ,
1275- routes : null ,
1325+ ... ( shouldClearRoutes && { routes : null } ) ,
12761326 } ,
12771327 } ) ;
12781328
@@ -1341,9 +1391,13 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
13411391 if ( hasPendingWaypoints ) {
13421392 optimisticViolations = optimisticViolations . filter ( ( violation ) => violation . name !== CONST . VIOLATIONS . NO_ROUTE ) ;
13431393 }
1344- if ( hasModifiedDistanceRate || hasModifiedDistance ) {
1394+ if ( hasModifiedDistanceRate || hasModifiedDistance || hasPendingWaypoints ) {
1395+ // Clear stale distance-related violations while the server reprocesses.
1396+ // The server will re-evaluate and re-add any that legitimately apply.
13451397 optimisticViolations = optimisticViolations . filter (
1346- ( violation ) => ! ( violation . name === CONST . VIOLATIONS . MODIFIED_AMOUNT && violation . data ?. type === CONST . MODIFIED_AMOUNT_VIOLATION_DATA . DISTANCE ) ,
1398+ ( violation ) =>
1399+ ! ( violation . name === CONST . VIOLATIONS . MODIFIED_AMOUNT && violation . data ?. type === CONST . MODIFIED_AMOUNT_VIOLATION_DATA . DISTANCE ) &&
1400+ violation . name !== CONST . VIOLATIONS . INCREASED_DISTANCE ,
13471401 ) ;
13481402 }
13491403
0 commit comments