Skip to content

Fix card-issued link resolving against the active route - #98406

Open
mukhrr wants to merge 5 commits into
Expensify:mainfrom
mukhrr:fix/95364
Open

Fix card-issued link resolving against the active route#98406
mukhrr wants to merge 5 commits into
Expensify:mainfrom
mukhrr:fix/95364

Conversation

@mukhrr

@mukhrr mukhrr commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The admin "replacement card" link (and the Spend > Chat card link) could open "Not Here" after a few clicks. getCardIssuedMessage built the link with createDynamicRoute and no base path, so it used Navigation.getActiveRoute(); when the chat message re-rendered while the card-details RHP was open, the href was rebuilt against the RHP's route and stopped resolving. The fix binds the link to the component's own screen via a new useDynamicRoute hook (built on React Navigation's useRoutePath()), so the href stays correct regardless of the active route.

Fixed Issues

$ #95364
PROPOSAL: #95364 (comment)

Tests

  1. Sign in as a workspace admin and make sure the Expensify Card feature is enabled on the workspace.
  2. Issue a virtual card to yourself, then go to Account > Wallet > Virtual card > Report virtual card fraud so a replacement virtual card is issued.
  3. Open the workspace chat and locate the "replacement card" hyperlink.
  4. Click the "replacement card" link → the card details open in the RHP.
  5. Close the card details and click the "replacement card" link again (repeat a few times).
  6. Verify it opens the card details every time and never shows the "Not Here" page.
  7. Open the same report from the Spend tab (Spend > Chat) and repeat steps 4-6, verifying the same.
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests. The link is built client-side, so the behavior is identical offline.

QA Steps

  1. Issue a virtual card for yourself.
  2. Go to Account > Wallet > Virtual card > Report virtual card fraud.
  3. Go to the workspace chat and click the "replacement card" hyperlink a few times.
  4. Verify the app opens the card details and never shows the "Not Here" page.
  • 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 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
android_app.mp4
Android: mWeb Chrome
android_web.mp4
iOS: Native
IOS-app.mp4
iOS: mWeb Safari
IOS-web.mp4
MacOS: Chrome / Safari
web.mp4

@mukhrr
mukhrr marked this pull request as ready for review August 12, 2026 10:05
@mukhrr
mukhrr requested review from a team as code owners August 12, 2026 10:05
@melvin-bot
melvin-bot Bot requested a review from dmkt9 August 12, 2026 10:05
@mukhrr
mukhrr marked this pull request as draft August 12, 2026 10:05
@melvin-bot
melvin-bot Bot requested review from trjExpensify and removed request for a team and trjExpensify August 12, 2026 10:05
@melvin-bot

melvin-bot Bot commented Aug 12, 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 removed the request for review from a team August 12, 2026 10:05
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/ReceiptAlternativeMethods.tsx 100.00% <100.00%> (ø)
...c/components/ReportActionItem/IssueCardMessage.tsx 91.66% <100.00%> (+0.36%) ⬆️
src/components/ReportWelcomeText.tsx 96.72% <100.00%> (ø)
src/hooks/useDynamicRoute.ts 100.00% <100.00%> (ø)
src/libs/ReportActionsUtils.ts 80.00% <100.00%> (+0.04%) ⬆️
src/components/BookTravelButton.tsx 81.81% <50.00%> (+0.23%) ⬆️
src/pages/workspace/companyCards/utils.tsx 26.86% <50.00%> (+0.17%) ⬆️
.../workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx 0.00% <0.00%> (ø)
src/hooks/usePayAndDowngrade.ts 0.00% <0.00%> (ø)
...es/inbox/report/ContextMenu/ContextMenuActions.tsx 40.48% <0.00%> (-0.12%) ⬇️
... and 5 more
... and 11 files with indirect coverage changes

@mukhrr
mukhrr force-pushed the fix/95364 branch 2 times, most recently from 4affc1c to 0d92b1f Compare August 12, 2026 10:25
@mukhrr
mukhrr marked this pull request as ready for review August 12, 2026 10:26
Comment thread tests/unit/ReportActionsUtilsTest.ts Outdated
getCardIssuedMessage built the admin card-details link with createDynamicRoute
and no base path, so it prepended Navigation.getActiveRoute(). The href is
rebuilt on every render, so an Onyx-driven re-render while an RHP is open over
the chat rebuilt it against the RHP's route and it fell through to Not Here.

Base the link on the component's own screen via React Navigation's useRoutePath,
wrapped in a new useDynamicRoute hook that returns a createDynamicRoute bound to
that path. The href is now identical on every render regardless of the active
route, and Spend > Chat is covered by the same change.
# Conflicts:
#	src/components/ReportActionItem/IssueCardMessage.tsx
#	src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
#	tests/unit/ReportActionsUtilsTest.ts
@dmkt9

dmkt9 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

@mukhrr The solution looks good. However, I think we should verify all usages of createDynamicRoute and replace them with useDynamicRoute where necessary.

@mukhrr

mukhrr commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@dmkt9 I did!

@mukhrr The solution looks good. However, I think we should verify all usages of createDynamicRoute and replace them with useDynamicRoute where necessary.

@dmkt9

dmkt9 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@dmkt9 I did!

@mukhrr I'm not sure. I did a quick search and found a few places that still need to be updated:

Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.TRAVEL_UPGRADE.path));

const contactMethodsHref = `${environmentURL}${addLeadingForwardSlash(createDynamicRoute(DYNAMIC_ROUTES.CONTACT_METHODS.path))}`;

const reportDetailsPath = reportRHPActiveRoute ? createDynamicRoute(DYNAMIC_ROUTES.REPORT_DETAILS.path, reportRHPActiveRoute) : createDynamicRoute(DYNAMIC_ROUTES.REPORT_DETAILS.path);

I think there are still many other usages that we need to check.

@mukhrr

mukhrr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@dmkt9 actually we have 477 createDynamicRoute calls in src/ as I mentioned before and I think 471 are fine already. 146 pass an explicit base, and 325 run at press time inside navigate / goBack, where the active route is the correct one and is the documented usage in NAVIGATION.md. Only the ones built during render and used later can go stale.

Fixed only 6 with exact reason:

  • ReceiptAlternativeMethods.tsx:38
  • ReportWelcomeText.tsx:102
  • MergeResultPage.tsx:81
  • DynamicReconciliationAccountSettingsPage.tsx:159
  • companyCards/utils.tsx:246 and :373

The first four use useDynamicRoute now. The last two only needed the backTo the function already takes, same as the QBO and Sage Intacct branches right beside them, leftover from #72352.

The four component/page sites built an href during render with no base path, so a
re-render while another route was on top rebuilt the link against that route. They
now take the base from useDynamicRoute.

getExportMenuItem is not a hook context; its NetSuite and QuickBooks Desktop
branches simply never read the backTo the function already accepts, unlike the
QuickBooks Online and Sage Intacct branches beside them.

ReportWelcomeText's getReportRHPActiveRoute workaround was a no-op: it returns
getActiveRoute() when a report is in the RHP and an empty string otherwise, so both
branches resolved to the same base as passing none.
@dmkt9

dmkt9 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@mukhrr I agree that we don't need to change the cases where createDynamicRoute is called inside onPress or onSelected callbacks. But what about cases where navigate is called inside a useEffect, like in BookTravelButton?

Couldn't that potentially trigger when the screen isn't focused?

@mukhrr

mukhrr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@dmkt9 I believe BookTravelButton cases run inside the bookATrip press handler, so they are not affected.

@dmkt9

dmkt9 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@dmkt9 I believe BookTravelButton cases run inside the bookATrip press handler, so they are not affected.

useEffect(() => {
if (!shouldResumeBookingRef.current || areTravelPersonalDetailsMissing(privatePersonalDetails)) {
return;
}
shouldResumeBookingRef.current = false;
bookATrip();
// eslint-disable-next-line react-hooks/exhaustive-deps -- we only want to trigger this effect when privatePersonalDetails changes
}, [privatePersonalDetails]);

These effects are triggered by external or async state rather than by the user
acting on the screen, so they can run while another route is on top. The route was
then built against that route and failed entryScreens validation, landing on Not
Here.

The triggers: an Uber connection completed outside the app, a bill API response
arriving while the hook stays mounted on the underlying screen, a backend error
reaching a step that already pushed the validate-code screen above itself, and the
server response to an ownership change.

BookTravelButton reaches this through its resume effect: the Onyx write for the
saved legal name can land before the back navigation finishes, and the second pass
skips the missing-details guard and continues into the branches below it.
@mukhrr

mukhrr commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

But what about cases where navigate is called inside a useEffect

@dmkt9 you are right. I thinnk we should fix all 15 useEffect cases

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.

2 participants