Fix stuck Save loading state when cancelling travel monthly limit reduction - #99344
Fix stuck Save loading state when cancelling travel monthly limit reduction#99344MelvinBot wants to merge 4 commits into
Conversation
Co-authored-by: Nyoman Jyotisa <nyomanjyotisa@users.noreply.github.com>
|
🤖 I attempted to run the testing steps on Web, but I was blocked before reaching the feature under test and could not verify the fix. Where I got stuck: Enabling Expensify Travel on the workspace is hard-blocked by the app's public-email-domain gate ( This is a test-account limitation (Melvin's session email is always a public Step results:
To fully QA this, a tester with a non-public (work-domain) email account is needed to reach the Monthly spend limit page and exercise the reduce-limit → Cancel flow. |
|
🤖 I attempted to run the testing steps on Android, but I was blocked before reaching the feature under test and could not verify the fix — the same blocker as on Web. Where I got stuck: Enabling Expensify Travel on the workspace is gated behind a work-email requirement. Tapping "Book travel" surfaces "Get started with Expensify Travel — You'll need to use your work email (e.g., name@company.com)... not your personal email (e.g., name@gmail.com)", and "Got it" just returns to the same screen. My test account uses a This is a test-account limitation (Melvin's session email is always a public Step results:
To fully QA this, a tester with a non-public (work-domain) email account is needed to reach the Monthly spend limit page and exercise the reduce-limit → Cancel flow. |
| enabledWhenOffline | ||
| shouldHideFixErrorsAlert | ||
| addBottomSafeAreaPadding | ||
| shouldShowLoadingImmediatelyOnPress={false} |
There was a problem hiding this comment.
@MelvinBot Could you please add focused regression coverage verifying that Cancel leaves the Save button enabled without updating the limit, while Confirm updates it and navigates back?
|
I looked into the failing checks — there's no code fix needed. The only two failures are human-process checks, not code:
All actual code checks are green — tests (all 8 jobs), typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, and every build/perf job pass. So nothing to push here. This PR just needs a reviewer to complete their checklist and approve. |
…t reduction Co-authored-by: Nyoman Jyotisa <nyomanjyotisa@users.noreply.github.com>
|
🤖 Added focused regression coverage in It renders
Both tests pass and the file lints clean: Test approach
|
…mport ordering Co-authored-by: Nyoman Jyotisa <nyomanjyotisa@users.noreply.github.com>
|
Fixed the failing Oxfmt check and pushed the change. The formatter flagged an import-ordering issue in the test file this PR added, The other remaining failure, |
| })); | ||
|
|
||
| jest.mock('@libs/actions/TravelBilling', () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
There was a problem hiding this comment.
@MelvinBot Could we type jest.requireActual and remove these ESLint disables? jest.requireActual<typeof TravelBillingActions>(...) works here without suppressions.
There was a problem hiding this comment.
Done in 336fc3d. Typed jest.requireActual<typeof TravelBillingActions>(...) and removed both @typescript-eslint/no-unsafe-assignment / no-unsafe-return disables — the mock factory is now fully typed with no suppressions.
One tweak from your exact snippet: jest.requireActual<typeof import('@libs/actions/TravelBilling')>(...) trips @typescript-eslint/consistent-type-imports (inline import() type annotations are forbidden), so I added a import type * as TravelBillingActions from '@libs/actions/TravelBilling' and referenced typeof TravelBillingActions as you suggested.
tests/ui/WorkspaceTravelBillingMonthlyLimitPageTest.tsx:41 — lint-changed passes clean.
|
Same as before — the two failing checks are human-process checks, not code, so there's nothing for me to push:
Every code check (all test jobs, typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, builds, perf) is passing. This PR just needs a reviewer to fill out their checklist and approve — no code fix is needed or possible from my side. |
|
The two remaining failures are human-process checks, not code — there's nothing for me to fix or push:
All code checks are green (all 8 test jobs, typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, and every build/perf job). This PR just needs a reviewer to complete their checklist and approve. |
Co-authored-by: Nyoman Jyotisa <nyomanjyotisa@users.noreply.github.com>
|
Same situation as before — nothing to push. The only two failing checks are human-process checks, not code:
Every code check is green — all 8 test jobs, typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, and all build/perf jobs. There's no code fix to make here. This PR just needs a reviewer to fill out their checklist and approve. |
|
Same as before — there's no code fix to push. The only two failing checks are human-process gates, not code:
All code checks (tests, typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, builds, perf) are passing. This PR just needs a reviewer to fill out the checklist and approve — nothing for me to change. |
|
Still the same two failures, and both are human-process checks — there's no code fix I can push:
Every code check (all 8 test jobs, typecheck, ESLint, Oxfmt, Storybook, React Compiler, knip, spellcheck, builds, perf) is green. This PR just needs a reviewer to fill out their checklist and approve — nothing on my end to fix. |



Explanation of Change
On the workspace Travel billing Monthly spend limit page, the Save button uses press-loading — an instant spinner shown the moment you press Save (
FormProviderdefaultsshouldShowLoadingImmediatelyOnPresstotrue).When you reduce the limit,
handleSubmitopens a confirmation modal after the press-loading spinner has already been shown. If you then tap Cancel, the flow returns early: it never navigates away and never sets any external Onyx loading flag. BecauseusePressLoadingonly clears itsisPressedstate when the externalisLoadingturnstrue, the submit callback throws, or the screen loses/regains navigation focus — and the confirm modal is a global overlay, not a navigation screen — none of those happen, so the spinner sticks on forever.This form's submit either navigates back immediately or opens a confirmation modal, and it has no external Onyx loading state to hand off to, so the instant press spinner serves no purpose here. Disabling it with
shouldShowLoadingImmediatelyOnPress={false}means the button reflects only the (always-false) external loading, so cancelling the reduce-limit warning leaves the Save button in its normal state.Fixed Issues
$ #99152
PROPOSAL: #99152 (comment)
Tests
// TODO: The human co-author must fill out the tests you ran before marking this PR as "ready for review".
// Please describe what tests you performed that validate your change worked, on all platforms.
Offline tests
Same as tests.
QA Steps
// TODO: These must be filled out, or the issue title must include "[No QA]."
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari