Skip to content

[$175] Distance - Map shows km instead of miles #104150

Open

Description

@vincdargento

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: v9.5.10-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from TestRail: https://github.com/Expensify/Expensify/issues/616079
Email or phone of affected tester (no customers): N/A
Issue reported by: Applause Internal Team

Action Performed:

  1. Navigate to staging.new.expensify.com.
  2. Create a USD workspace with Distance rates enabled.
  3. Open Distance rates → Settings → Unit and select Kilometers.
  4. Navigate to Inbox → FAB → Start chat and select another user.
  5. In the 1:1 chat, tap + → Track distance → Map.
  6. Enter valid waypoints to create a route.

Expected Result:

The unit displayed on the green route line should be miles (mi), not kilometers.

Actual Result:

The unit displayed on the green route line is kilometers (km) instead of miles.

Workaround:

N/A

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Bug7262437_1789648895173.bandicam_2026-09-17_15-31-09-910.mp4
Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 15, 2026
  2. MelvinBot commented on Oct 15, 2026

    @MelvinBot
    Contributor

    ⚠️ Preliminary analysis (low confidence): treat this as a starting point, not a conclusion.

    I think the map label is reading the unit from the wrong source. The label's unit is set in DistanceRequestFooter.tsx:69-72. There are two ways it could show km here:

    1. Workspace fallback. If the draft does not carry the P2P rate ID, line 69 uses getDefaultMileageRate(policy ?? activePolicy). In a DM policy is undefined, so the label takes the active workspace's unit (km). The confirmation page uses getRate, which has no activePolicy fallback, so it would show mi.
    2. Backend P2P rate. If the draft does carry FAKE_P2P_ID, the unit comes from the stored GetDefaultP2PMileageRate response (DistanceRequestUtils.ts:302-314). If that response returns km, the confirmation page and the saved expense would show km too. That would be a backend issue.

    From the code, a 1:1 DM should take path 2. "Track distance" in a DM starts a SUBMIT request, and getCustomUnitRateID seeds FAKE_P2P_ID. I couldn't confirm which path fires because the web test session failed to load, so I couldn't reproduce the bug.

    To confirm: repeat the steps and check the unit on the confirmation page.

    • Confirmation shows mi: it's path 1. Fix the footer so it uses DistanceRequestUtils.getRate({transaction, policy, personalPolicyOutputCurrency: personalPolicy?.outputCurrency}), as the native GPS page does. Then remove the activePolicy lookup.
    • Confirmation shows km: check the Onyx key defaultP2PMileageRate. If its unit is km for a USD account, the fix belongs in the backend, and an internal engineer needs to look at it.
    Investigation details

    Next Steps: Reply with @MelvinBot <your feedback> to refine this analysis.


    view run · no recording available

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 15, 2026
  4. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @mkhutornyi (External)

  5. changed the title [-]Distance - Map shows km instead of miles[/-] [+][$175] Distance - Map shows km instead of miles[/+] on Oct 15, 2026
  6. melvin-bot commented on Oct 15, 2026

    @melvin-bot
  7. camesenin commented on Oct 15, 2026

    @camesenin

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When a user sets the distance rate unit to Kilometers on a workspace, navigates to a 1:1 direct message (DM), and creates a distance request (+ -> Track distance -> Map), the unit displayed on the map route line is shown as kilometers (km) instead of miles (mi).

    What is the root cause of that problem?

    In src/components/DistanceRequest/DistanceRequestFooter.tsx:69-72:

    1. DistanceRequestFooter falls back to activePolicy when policy is undefined:
    const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
    const policyCurrency = (policy ?? activePolicy ?? personalPolicy)?.outputCurrency ?? CONST.CURRENCY.USD;
    const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
    1. In a 1:1 DM chat, policy is undefined (personal chat, not workspace).
    2. On a newly initialized draft transaction (startDistanceRequest), isCustomUnitRateIDForP2P(transaction) evaluates to false because customUnitRateID is not yet set.
    3. mileageRate falls back to defaultMileageRate, which reads the unit from activePolicy (active workspace), incorrectly displaying km on the map route.

    What changes do you think we should make in order to solve the problem?

    In src/components/DistanceRequest/DistanceRequestFooter.tsx:

    1. Remove activePolicyID and activePolicy.
    2. Use DistanceRequestUtils.getRate to resolve mileageRate and unit, matching IOURequestStepDistanceGPS:
    const mileageRate = DistanceRequestUtils.getRate({
        transaction,
        policy,
        personalPolicyOutputCurrency: personalPolicy?.outputCurrency,
    });
    const {unit} = mileageRate ?? {};

    What alternative solutions did you explore? (Optional)

    None.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  8. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  9. janiya2 commented on Oct 15, 2026

    @janiya2

    Contributor details
    Your Expensify account email: janithad2@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/janithad

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When a user has an active workspace with Distance rates unit configured to Kilometers (km), and navigates to a 1:1 personal DM chat and creates a distance request (+ → Track/Record distance → Map), the green route line on the map incorrectly displays kilometers (km) instead of miles (mi). When navigating to the confirmation page, it displays miles (mi), creating an inconsistent state between the map route line and the confirmation/transaction amount.


    What is the root cause of that problem?

    In src/components/DistanceRequest/DistanceRequestFooter.tsx:

    const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
    const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID);
    const activePolicy = usePolicy(activePolicyID);
    const personalPolicy = usePolicy(personalPolicyID);
    ...
    const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
    const policyCurrency = (policy ?? activePolicy ?? personalPolicy)?.outputCurrency ?? CONST.CURRENCY.USD;
    const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
    const {unit} = mileageRate ?? {};

    There are two interrelated flaws causing this:

    1. Premature isCustomUnitRateIDForP2P check on draft transactions:
      During the initial map step in a 1:1 chat, startDistanceRequest only clears the draft transaction and navigates to the map screen. At this point, transaction.comment?.customUnit?.customUnitRateID has not yet been set to CONST.CUSTOM_UNITS.FAKE_P2P_ID. Thus, isCustomUnitRateIDForP2P(transaction) returns false.

    2. Erroneous fallback to activePolicy for personal/DM chats:
      In a 1:1 personal chat, report.policyID is undefined. Consequently, usePolicyForTransaction returns policy: undefined (because personal 1:1 chats are not linked to any workspace policy).
      Because policy is undefined and isCustomUnitRateIDForP2P(transaction) is false, DistanceRequestFooter falls back to activePolicy on line 69:
      DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy)
      activePolicy represents whichever workspace the user currently has active in their app (which in this reproduction has unit set to Kilometers). As a result, the personal 1:1 chat distance request pulls in the workspace's default rate with unit: 'km' and renders km on the map route line.

      On the confirmation screen, useDistanceRequestState derives the rate via DistanceRequestUtils.getRate, which does not have an activePolicy fallback and correctly defaults to miles (mi).


    What changes do you think we should make in order to solve the problem?

    In src/components/DistanceRequest/DistanceRequestFooter.tsx:

    1. Remove the fallback to activePolicy. Personal / 1:1 chat requests must not inherit unrelated workspace settings.
    2. If policy is not defined (or empty), or if isCustomUnitRateIDForP2P(transaction) is true, treat the request as P2P and resolve the rate via DistanceRequestUtils.getRateForP2P(policyCurrency, transaction).
    3. Derive unit consistently via DistanceRequestUtils.getDistanceUnit(transaction, mileageRate).
    diff --git a/src/components/DistanceRequest/DistanceRequestFooter.tsx b/src/components/DistanceRequest/DistanceRequestFooter.tsx
    index b70422bcc..9812a34ef 100644
    --- a/src/components/DistanceRequest/DistanceRequestFooter.tsx
    +++ b/src/components/DistanceRequest/DistanceRequestFooter.tsx
    @@ -20,6 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
     import type {Policy} from '@src/types/onyx';
     import type {WaypointCollection} from '@src/types/onyx/Transaction';
     import type Transaction from '@src/types/onyx/Transaction';
    +import {isEmptyObject} from '@src/types/utils/EmptyObject';
     import type TransactionState from '@src/types/utils/TransactionStateType';
     
     import type {StyleProp, ViewStyle} from 'react-native';
    @@ -57,19 +58,18 @@ function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPa
         const {translate} = useLocalize();
         const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus', 'History']);
         const [reusableDistanceRoutes] = useOnyx(ONYXKEYS.REUSABLE_DISTANCE_ROUTES);
    -    const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
         const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID);
    -    const activePolicy = usePolicy(activePolicyID);
         const personalPolicy = usePolicy(personalPolicyID);
         const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN);
     
         const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
         const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length;
         const lastWaypointIndex = numberOfWaypoints - 1;
    -    const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
    -    const policyCurrency = (policy ?? activePolicy ?? personalPolicy)?.outputCurrency ?? CONST.CURRENCY.USD;
    -    const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
    -    const {unit} = mileageRate ?? {};
    +    const policyCurrency = policy?.outputCurrency ?? personalPolicy?.outputCurrency ?? CONST.CURRENCY.USD;
    +    const isP2P = !policy || isEmptyObject(policy) || isCustomUnitRateIDForP2P(transaction);
    +    const mileageRate = isP2P ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : DistanceRequestUtils.getDefaultMileageRate(policy);
    +    const unit = DistanceRequestUtils.getDistanceUnit(transaction, mileageRate);
         const primaryRoute = transaction?.routes?.[CONST.TRANSACTION.DEFAULT_ROUTE_KEY];
         const alternateRoute = transaction?.routes?.[CONST.TRANSACTION.ALTERNATE_ROUTE_KEY];
         const isAlternateDirectionSelected = getSelectedRouteKey(transaction) === CONST.TRANSACTION.ALTERNATE_ROUTE_KEY;

    What alternative solutions did you explore? (Optional)

    We could also pass isFakeP2PRate from IOURequestStepDistanceMap by inspecting whether !isPolicyExpenseChatUtil(report), but handling it directly in DistanceRequestFooter.tsx via !policy || isEmptyObject(policy) || isCustomUnitRateIDForP2P(transaction) is cleaner and encapsulates the rate resolution self-sufficiently without leaking unrelated activePolicy data into non-workspace requests.

  10. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  11. itappskitkool commented on Oct 15, 2026

    @itappskitkool

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In a 1:1 DM chat, Track distance on the Map tab shows the route distance in kilometers on the green route line, even though the expected unit for this request is miles. This happens after a USD workspace is created and its Distance rates unit is set to Kilometers, and that workspace is not the workspace for the DM request.

    What is the root cause of that problem?

    DistanceRequestFooter in src/components/DistanceRequest/DistanceRequestFooter.tsx is the only distance surface that does not use DistanceRequestUtils.getRate. It computes the map unit from DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy) and also derives policyCurrency from policy ?? activePolicy ?? personalPolicy. In a 1:1 DM, policy is undefined, so it falls back to activePolicy, which is the unrelated USD workspace the user just set to Kilometers. That gives a km unit, which is passed as unit to DistanceMapView, then to Directions and DistanceSymbol, so the route label shows km. The confirmation page, DynamicIOURequestStepDistance, and the native GPS page all use DistanceRequestUtils.getRate({transaction, policy, personalPolicyOutputCurrency}), which resolves the P2P rate for a non policy chat and returns miles for USD, so the map disagrees with the rest of the flow.

    What changes do you think we should make in order to solve the problem?

    In DistanceRequestFooter.tsx, replace the getDefaultMileageRate(policy ?? activePolicy) calculation with DistanceRequestUtils.getRate({transaction, policy, personalPolicyOutputCurrency: personalPolicy?.outputCurrency}), the same call used in IOURequestStepDistanceGPS/index.native.tsx and in DynamicIOURequestStepDistance.tsx, and use its unit for DistanceMapView. Remove the now unused activePolicy lookup using NVP_ACTIVE_POLICY_ID and usePolicy(activePolicyID), and keep personalPolicy which the component already loads. This keeps workspace distance requests working, because getRate still resolves the selected or default rate from the real policy when there is one, and it keeps P2P requests working, because getRate already handles the FAKE_P2P_ID case through getRateForP2P.

    What alternative solutions did you explore? (Optional)

    I considered only skipping the activePolicy fallback when the transaction is a P2P transaction, checked with isCustomUnitRateIDForP2P. That would fix part of this case, but it would leave the footer using a different rate calculation from every other distance surface and could still pick the wrong unit in other no policy cases. Using getRate in the footer is smaller in the long run and matches the existing pattern.

  12. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    📣 @itappskitkool! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  13. taieddie commented on Oct 15, 2026

    @taieddie

    Contributor details
    Your Expensify account email: taieddie8@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01e51b3f9ff738cf94

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When tracking distance in a 1:1 chat after configuring distance rate units to kilometers in an active workspace, the route line on the map displays kilometers instead of miles.

    What is the root cause of that problem?

    In src/components/DistanceRequest/DistanceRequestFooter.tsx (lines 69-72), defaultMileageRate falls back to activePolicy using DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy). In a 1:1 chat, policy is undefined because 1:1 chats do not belong to a policy.

    At the same time, the draft transaction in the map step has not set transaction.comment.customUnit.customUnitRateID to CONST.CUSTOM_UNITS.FAKE_P2P_ID yet, so isCustomUnitRateIDForP2P(transaction) returns false. DistanceRequestFooter falls back to defaultMileageRate derived from activePolicy. It leaks the active policy. Because activePolicy was set to kilometers, unit resolves to km instead of the standard P2P default unit (mi).

    What changes do you think we should make in order to solve the problem?

    1. In src/components/DistanceRequest/DistanceRequestFooter.tsx:
    • Remove the Onyx connection to ONYXKEYS.NVP_ACTIVE_POLICY_ID and remove activePolicy.
    • Update defaultMileageRate to evaluate DistanceRequestUtils.getDefaultMileageRate(policy) directly without falling back to activePolicy.
    • Update policyCurrency fallback to use (policy ?? personalPolicy)?.outputCurrency ?? CONST.CURRENCY.USD.
    • Determine whether the request is P2P by checking !policy || isCustomUnitRateIDForP2P(transaction). When true, select DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) as the mileageRate, resolving the unit to miles.

    What alternative solutions did you explore? (Optional)

    I considered setting transaction.comment.customUnit.customUnitRateID to CONST.CUSTOM_UNITS.FAKE_P2P_ID upon draft creation in 1:1 chats. However, keeping the fallback to activePolicy in DistanceRequestFooter is conceptually wrong for personal chats and leaves the door open to leaking workspace state.

  14. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  15. itappskitkool commented on Oct 15, 2026

    @itappskitkool

    Contributor details
    Your Expensify account email: nitinladse@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/nitinl2

  16. melvin-bot commented on Oct 15, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  17. tnclaudecode commented on Oct 17, 2026

    @tnclaudecode

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In a 1:1 DM, Track distance → Map shows the route in km for a user whose workspace distance unit was set to km, when miles was expected.

    What is the root cause of that problem?

    The workspace's km setting doesn't reach this label. In a 1:1 DM:

    • "Track distance" starts a SUBMIT request. The DM has no real policy (its policyID is _FAKE_), so policy is undefined.
    • When the Map tab opens, useResetIOUType → initMoneyRequest creates the draft with customUnitRateID = FAKE_P2P_ID, because getCustomUnitRateID returns the P2P ID when there's no policy. It sets iouRequestType in the same write, and a route (and so the green label) is only fetched after that.
    • So DistanceRequestFooter always takes the getRateForP2P branch here, never the activePolicy fallback. Its unit comes from the stored GetDefaultP2PMileageRate response, which Auth picks from the user's personal currency.

    So the label shows km because the default P2P rate for this account's personal currency is in km. The confirmation page and the saved expense read the same value, so they will show km too. Replacing the footer's logic with DistanceRequestUtils.getRate(), as suggested above, gives the same result for this flow, because getRate takes the same getRateForP2P branch for a P2P draft.

    What changes do you think we should make in order to solve the problem?

    1. Check the tester account's personal policy outputCurrency and the stored defaultP2PMileageRate. If the currency maps to km, this is expected behaviour (or a product decision about P2P units), not an App bug. If the currency is USD and Auth still returns km, it's a backend bug in the P2P mapping.
    2. Optional hardening, as a separate change: have the map step work out the unit once (with DistanceRequestUtils.getRate, using the default expense policy for global-create flows, like the GPS page does) and pass it into DistanceRequestFooter, instead of the footer re-deriving it from activePolicy. Also store distanceUnit when an alternate route is selected, so quantity can't be read in the wrong unit.

    What alternative solutions did you explore? (Optional)

    Switching the footer to getRate() alone. It has no effect on this DM flow. It also makes "Track distance" from the global FAB show miles on the map and then km on confirmation for users whose default workspace uses km, because those drafts have no rate ID yet and getRate without a policy falls back to miles.

  18. melvin-bot commented on Oct 17, 2026

    @melvin-bot

    📣 @tnclaudecode! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions