Skip to content

Add alternate route for map-based mileage expenses - #96169

Open
GCyganek wants to merge 32 commits into
Expensify:mainfrom
software-mansion-labs:@GCyganek/alternate-routes
Open

Add alternate route for map-based mileage expenses#96169
GCyganek wants to merge 32 commits into
Expensify:mainfrom
software-mansion-labs:@GCyganek/alternate-routes

Conversation

@GCyganek

@GCyganek GCyganek commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Adds alternate route selection for map-based distance expenses. The map now renders both routes returned by the BE and lets the user tap one to pick it; the pick is stored in comment.selectedRouteKey and recovered for saved expenses by distance-matching customUnit.routeDistanceMeters. selectedRouteDistance is sent on create/track/update so the BE uses the chosen route.

Fixed Issues

$ #90658
PROPOSAL: N/A

Tests

1. Create a map distance expense on the alternate route (check both workspace and selfDM expense)

  1. Open the FAB and choose Track distance → Map.
  2. Enter a start and a finish waypoint that have more than one driving route (e.g. two cities connected by both a highway and a secondary road).
  3. Wait for the map to render and verify that two routes are drawn: the selected one in the darker green with a dark distance label, and the alternate one in the lighter green with a light distance label.
  4. Tap the alternate (lighter) route on the map.
  5. Verify that the tapped route becomes the selected one (colors swap) and that the distance/amount shown on the button and on the confirmation screen update to that route's distance.
  6. Tap Next, then submit the expense.
  7. Open the created expense and verify that its distance, merchant string and amount match the alternate route, not the primary one.
  8. Verify that the expense preview map highlights the alternate route as the selected one.

2. Switch the route on an already-created expense

  1. Using the expense created in test 1, open it and click the distance/route field to open the distance edit page.
  2. Verify that the previously chosen alternate route is still shown as the selected one (this is recovered from the saved route distance, so it also holds after a page reload or a fresh login).
  3. Tap the other route on the map and click Save.
  4. Verify that the expense amount, merchant and distance update to the newly selected route, and that a "changed the distance" system message is posted in the expense thread with the correct old/new amounts and the expense currency (not USD) for a non-USD workspace.
  5. Reopen the distance edit page and verify the newly selected route is still marked as selected.
  6. Open the distance edit page again and click Save without changing anything, and verify that no update request is sent and no system message is posted.

3. Manual distance override and waypoint edits

  1. Open an existing map distance expense, go to the Manual tab, type a distance different from both routes and Save.
  2. Reopen the edit page and verify the Manual tab still shows the typed value and the map still shows both routes with the previously selected one highlighted (a manual override must not read as a route switch).
  3. Tap the alternate route on the map, verify the manual input follows the newly selected route's distance, then Save and verify the expense uses the route distance rather than the old typed value.
  4. Reopen the edit page, change one of the waypoints and verify both routes are cleared, refetched and the selection resets to the primary route.
  5. Change a waypoint, then press the back button without saving and verify the discard-changes modal appears. Repeat with only a route tap (no waypoint change) and verify the modal also appears.
  6. Split the expense (Split expense) and verify each split item's distance, amount and merchant are derived from the selected route, and that a manual override on the parent expense is preserved rather than replaced by the route distance when editing an unrelated split field.

Offline tests

Map is not loaded in offline mode so user can't change route in offline mode

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Screen.Recording.2026-08-03.at.17.12.16.mov
Screen.Recording.2026-08-03.at.17.11.29.mov
Screen.Recording.2026-08-03.at.17.09.28.mov
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-08-03.at.16.51.42.mov
Screen.Recording.2026-08-03.at.17.02.38.mov
Screen.Recording.2026-08-03.at.17.03.19.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-03.at.16.24.20.mov
Screen.Recording.2026-08-03.at.16.26.13.mov
Screen.Recording.2026-08-03.at.16.30.42.1.mov

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/CONST/index.ts 94.81% <ø> (ø)
src/components/MapView/utils.ts 37.97% <100.00%> (+7.41%) ⬆️
src/components/ViolationMessages.tsx 0.00% <ø> (ø)
src/libs/DebugUtils.ts 68.82% <ø> (ø)
src/libs/ReportUtils.ts 86.68% <100.00%> (ø)
src/libs/TransactionUtils/getDistanceInMeters.ts 100.00% <100.00%> (ø)
src/libs/TransactionUtils/getSelectedRouteKey.ts 100.00% <100.00%> (ø)
src/libs/Violations/ViolationsUtils.ts 79.67% <ø> (ø)
src/libs/actions/IOU/Split.ts 72.46% <ø> (ø)
src/libs/actions/IOU/SplitExpenseItems.ts 95.41% <100.00%> (+0.68%) ⬆️
... and 23 more
... and 17 files with indirect coverage changes

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ This PR is possibly changing native code and/or updating libraries, it may cause problems with HybridApp. Please check if any patch updates are required in the HybridApp repo and run an AdHoc build to verify that HybridApp will not break. Ask Contributor Plus for help if you are not sure how to handle this. ⚠️

@GCyganek

Copy link
Copy Markdown
Contributor Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1aa5fc97f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/TransactionUtils/index.ts Outdated
Comment thread src/components/DistanceRequest/DistanceRequestFooter.tsx Outdated
Comment thread src/libs/actions/Transaction.ts
Comment thread src/components/MapView/MapViewImpl.web.tsx Outdated
@GCyganek

Copy link
Copy Markdown
Contributor Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d602e2a1c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const {unit} = mileageRate ?? {};
const isAlternativeDirectionSelected = transaction?.comment?.selectedRouteKey === 'route1';
const handleRouteSelection = (isAlternative: boolean) => {
setSelectedRoute(transaction?.transactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID, isAlternative ? 'route1' : 'route0', transactionState);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist alternate route edits on save

When this handler runs in the edit flow for an existing map-distance expense, it only merges selectedRouteKey locally. The save path in IOURequestStepDistance still returns early when waypoint addresses and manual quantity are unchanged, so tapping the alternate route and pressing Save sends no updateMoneyRequestDistance/selected-route data to the backend; the report can keep the old route/amount after sync even though the editor appeared to save the alternate selection. Include the selected route change in edit change detection and the update payload.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I'm aware, will fix this

@github-actions

Copy link
Copy Markdown
Contributor

🚧 JmillsExpensify has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@JmillsExpensify

Copy link
Copy Markdown
Contributor

Very cool! It works super well. Both of these routs should have been flagged, as I constantly get the same on Google Maps.
CleanShot 2026-07-29 at 18 09 10@2x

@GCyganek

Copy link
Copy Markdown
Contributor Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1eed358e66

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/TransactionUtils/index.ts Outdated
@GCyganek

Copy link
Copy Markdown
Contributor Author

@codex please review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 63d2059aa7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/ReportUtils.ts
Comment thread src/components/MapView/DistanceSymbol.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ebc582da2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/MapView/MapViewImpl.web.tsx
@GCyganek
GCyganek marked this pull request as ready for review August 3, 2026 15:07
@GCyganek
GCyganek requested review from a team as code owners August 3, 2026 15:07
@melvin-bot
melvin-bot Bot requested a review from dmkt9 August 3, 2026 15:07
@melvin-bot

melvin-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

@dmkt9 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from trjExpensify and removed request for a team August 3, 2026 15:07
Comment thread src/components/MapView/MapViewTypes.ts
Comment thread src/components/MapView/MapViewTypes.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76b40d042b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

linkedTrackedExpenseReportAction,
linkedTrackedExpenseReportID,
customUnitRateID,
selectedRouteDistance,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve route choice when moving tracked expenses

When the selected-route distance is only carried in the retry/default TrackExpense payload here, tracked-distance moves still lose it: the CATEGORIZE, SHARE, and SUBMIT branches below rebuild transactionParams without selectedRouteDistance, and moveTrackedExpenseToPolicy/shareTrackedExpense only spread those params into the API request. For a tracked map expense created on route1, later submitting/categorizing/sharing it to a workspace can therefore send only the waypoints and let the backend fall back to route0, changing the moved expense's distance/amount. Include this field in those action-specific transactionParams as well.

Useful? React with 👍 / 👎.

@shawnborton

Copy link
Copy Markdown
Contributor

Let us know when this is ready for some builds.

@GCyganek

GCyganek commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@shawnborton feel free to build and test

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🚧 shawnborton has triggered a test Expensify/App build. You can view the workflow run here.

@JmillsExpensify
JmillsExpensify requested review from JmillsExpensify and removed request for trjExpensify August 3, 2026 15:43
@JmillsExpensify

Copy link
Copy Markdown
Contributor

Removed Tom since this is my assigned initiative.

@shawnborton

Copy link
Copy Markdown
Contributor

Hmm the app immediately crashes when I try to test the iOS build. Any ideas?

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still looks great!

Copy link
Copy Markdown
Contributor

Interesting. I didn't see that in the last test.

@shawnborton

Copy link
Copy Markdown
Contributor

Visually this looks good, but the behavior of clicking on the miles badge and having it convert to KM feels weird. I feel like when you first click the alternate route, it should just select the alt route. Maybe clicking it a second time can convert the distance?

Also, we should use a pointer cursor here instead of a drag cursor when you go to hover over the alt line.

I also wonder if we need a hover state for the line but we can tackle that as a follow up if we want to.

CleanShot.2026-08-03.at.11.55.47.mp4

Copy link
Copy Markdown
Contributor

Visually this looks good, but the behavior of clicking on the miles badge and having it convert to KM feels weird. I feel like when you first click the alternate route, it should just select the alt route. Maybe clicking it a second time can convert the distance?

Ah interesting. I didn't run into this. I like this suggestion. Honestly I'd prefer to remove it for the v1, though I can see how it's useful at this stage because you haven't selected the workspace yet, so we don't really know which unit to use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants