You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$175] Expensify Card - Next button replaced with loading button after enabled then disabled rule #104509
Version Number: 9.5.15-1 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#103365 Email or phone of affected tester (no customers):applausetester@applause.expensifail.com Issue reported by: Applause Internal Team Bug source: Pull Request QA execution Device used: Windows 11 / Chrome App Component: Other
Please re-state the problem that we are trying to solve in this issue.
On the Issue card "Set card rules" step, if you turn on Add spend rule, click Next without setting a rule, and then turn the toggle off, the Next button stays a spinner. You can't press it, so you're stuck.
What is the root cause of that problem?
Clicking Next turns on the form's "spinner on press" state. The step's submit handler then stops early because no spend rule is set, so it never moves on. Nothing clears the spinner after that early stop.
isPressed is cleared in only three cases: the handler throws, the screen regains focus, or an external isLoading turns on. The early return in SetSpendRulesStep's submit triggers none of them (SetSpendRulesStep.tsx:190-196).
What changes do you think we should make in order to solve the problem?
Pass shouldShowLoadingImmediatelyOnPress={false} to the FormProvider in SetSpendRulesStep (SetSpendRulesStep.tsx:273-281). Next here only writes local Onyx data, so the user never waits long enough to need a spinner. Other forms already do this, for example RulePageBase.tsx and ReportAddApproverPage.tsx.
What alternative solutions did you explore? (Optional)
Fix it once in the shared hook: let onSubmit return false for "did not submit," and have startWithLoading clear isPressed when that happens. This would protect any other form whose submit handler stops early. It touches shared form components, so it's a bigger change than this bug needs.
Investigation notes
Confidence: high on the mechanism, from tracing the code. I didn't reproduce it in a running app.
Why it sometimes recovers: opening a rule sub-page (for example, "Max amount") and coming back refocuses the screen. The useFocusEffect in usePressLoading then clears the spinner.
validate in this step checks only the expiration dates. It passes with the toggle on and no rule set, so the press reaches submit.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/104509) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
changed the title [-]Expensify Card - Next button replaced with loading button after enabled then disabled rule[/-][+][$175] Expensify Card - Next button replaced with loading button after enabled then disabled rule[/+]on Oct 20, 2026
Please re-state the problem that we are trying to solve in this issue.
On the "Set card rules" step of issuing an Expensify Card, clicking "Next" with the "Add spend rule" toggle ON (but no rule actually configured) leaves the "Next" button permanently stuck in a loading state, even if the user subsequently turns the toggle OFF.
What is the root cause of that problem?
The root cause is an unhandled early return inside the form's onSubmit handler combined with Expensify's usePressLoading hook.
When the user clicks "Next", FormProvider.tsx triggers startWithLoading, which immediately sets the button's isPressed state to true to show the spinner.
The execution passes to submit in src/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsx.
Because the toggle is ON but no limit is set, lines 190-196 trigger an early return:
Because the onSubmit function returns undefined instead of a rejected Promise or throwing an error, usePressLoading.ts assumes the submission was "successful." Since the screen did not unmount or lose focus (which are the only other ways the spinner clears), the isPressed state is permanently orphaned as true.
What changes do you think we should make in order to solve the problem?
We should not disable the loading spinner (as MelvinBot suggested), as that degrades the standard form UX. Instead, the onSubmit early return must inform the FormProvider that the submission was aborted so the usePressLoading hook naturally clears the spinner.
In src/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsx, update the submit function to return a rejected Promise when the spend rule guard is hit:
// src/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsxconstsubmit=()=>{if(hasSpendRule&&!spendRule?.amount){setRuleError(true);returnPromise.reject(newError('Spend rule amount is required'));// FormProvider catches this and clears the spinner}if(hasSpendRule){IssueNewCardFlow.setIssueNewCardData({// ...
Because FormProvider wraps the onSubmit call in a try/catch and .catch() chain, rejecting the promise natively resolves the stuck spinner while maintaining the required UX loading state for successful submissions.
What alternative solutions did you explore? (Optional)
MelvinBot suggested passing shouldShowLoadingImmediatelyOnPress={false} to the FormProvider. This was rejected because it removes visual feedback from the button entirely. While IssueNewCardFlow Onyx writes are fast, standardizing on no-spinner forms creates UX inconsistency across the app. Rejecting the Promise fixes the root cause at the state-machine level without compromising the UI feedback loop.
Please re-state the problem that we are trying to solve in this issue.
In the Issue Expensify Card flow ("Set card rules" step), enabling Add spend rule, clicking Next without configuring a rule, and then turning the toggle off leaves the Next button permanently locked in a loading spinner state, preventing progression.
What is the root cause of that problem?
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:
FormProvider uses usePressLoading with shouldShowLoadingImmediatelyOnPress={true} by default (src/components/Form/FormProvider.tsx:156, 355).
When the user taps Next, startWithLoading synchronously sets isPressed = true, rendering the loading spinner on the button.
When no rules are selected, submit() executes an early return.
Crucially, isPressed in usePressLoading only resets when an async promise finishes, the handler throws, or the screen loses/regains focus. An early return leaves isPressed = true permanently.
In SetSpendRulesStep.tsx:111-117, toggling Add spend rule off only calls setCustomErrorMessage(''), but never resets isPressed, leaving the button permanently locked as a spinner.
What changes do you think we should make in order to solve the problem?
Disable immediate press loading on SetSpendRulesStep.tsx:
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:273-281:
Pass shouldShowLoadingImmediatelyOnPress={false} to FormProvider:
Why this is optimal: Submitting this step only writes local Onyx draft state before navigating to the next step, which completes in milliseconds and never requires an artificial blocking press spinner. Other local rule forms throughout the app (such as RulePageBase.tsx and ReportAddApproverPage.tsx) follow this exact pattern.
Clean error and state reset on toggle change:
In SetSpendRulesStep.tsx:111-117, reset hasSpendRules and explicitly clear local validation errors when toggled off.
What specific scenarios should we cover in automated tests?
In tests/unit/SetSpendRulesStepTest.tsx:
Render SetSpendRulesStep with toggle ON, submit without selecting rules, assert validation error appears and the submit button remains interactive (not locked in a spinner).
Toggle OFF, assert error is dismissed and tapping Next proceeds to the next step successfully.
Please re-state the problem that we are trying to solve in this issue.
On the Issue card "Set card rules" step, turning on "Add spend rule", clicking Next without configuring a rule, then turning the toggle off leaves the Next button permanently stuck in a loading spinner state, blocking progression.
What is the root cause of that problem?
FormProvider shows a spinner on press by default (shouldShowLoadingImmediatelyOnPress = true, src/components/Form/FormProvider.tsx:156). startWithLoading sets isPressed = true before running onSubmit (FormProvider.tsx:355).
isPressed is cleared in only three cases: the handler throws, the screen regains focus, or an external isLoading turns on.
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx, submit early-returns when no rule is configured:
The early return triggers none of the three reset paths, so the spinner never clears. Turning the toggle off only clears the error text (SetSpendRulesStep.tsx:111-117); later presses are ignored (FormProvider.tsx:311).
What changes do you think we should make in order to solve the problem?
Pass shouldShowLoadingImmediatelyOnPress={false} to the FormProvider in SetSpendRulesStep (lines 273-281). Next here only writes local Onyx data, so the user never waits long enough to need a spinner; precedent exists in RulePageBase.tsx and ReportAddApproverPage.tsx. This removes the stuck-spinner state for every early-return path rather than patching each one. I will re-verify the full toggle on -> Next -> error -> toggle off -> Next flow after the change.
Tests: submit with the toggle on and no rule set, assert the button is not left in a loading state after the error appears; turn the toggle off, press Next, assert navigation proceeds.
Please re-state the problem that we are trying to solve in this issue.
On the Issue Expensify Card "Set card rules" step, the user turns on Add spend rule, presses Next without configuring a rule, and sees the validation error. If they then turn the toggle off, the Next button stays replaced by a loading spinner. It can't be pressed again, so the user is stuck and can't continue the card issuing flow.
What is the root cause of that problem?
FormProvider shows a spinner as soon as the submit button is pressed. Nothing clears that spinner when the step's submit handler stops early.
FormProvider defaults shouldShowLoadingImmediatelyOnPress to true (FormProvider.tsx:156). On press it calls startWithLoading(runSubmit) (FormProvider.tsx:355). That sets isPressed = true in usePressLoading before onSubmit runs, so the button renders a spinner.
SetSpendRulesStep.validate only checks the expiration dates. With the toggle on and no rule set, it returns no errors, so the press goes through to submit.
In SetSpendRulesStep.tsx:190-196, submit runs an ad-hoc check and returns early:
isPressed is only cleared in these cases: the handler throws synchronously, the screen regains focus (useFocusEffect), or an external isLoading flips on. The early return triggers none of them, so isPressed stays true.
Toggling the rule off (handleToggleSpendRules, lines 111-117) only clears the error text. It never touches FormProvider's press state. The button stays a spinner, and FormProvider drops later presses because of its if (isLoading) return; guard (FormProvider.tsx:~311).
The spinner-on-press default came from PR #92498. The early return predates it and was harmless until then. PR #103365, the one under QA when this was found, is unrelated.
The Promise.reject suggestion would not work. With the default DISMISS_THEN_SUBMIT keyboard behavior, onSubmit is invoked inside KeyboardUtils.dismiss().then(...). startWithLoading doesn't chain that promise, so a rejection would be unhandled and would not clear isPressed.
What changes do you think we should make in order to solve the problem?
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx, pass shouldShowLoadingImmediatelyOnPress={false} to the FormProvider (around lines 273-281):
No latency to mask.submit only writes local draft data to Onyx (setIssueNewCardStepAndData) and navigates to the next step. No network call happens, so an immediate press spinner adds nothing.
Covers every early-return path. With the flag off, FormProvider never enters the press-loading state for this form. This fixes the reported flow and any other early exit in submit, with no per-path cleanup.
Established precedent. The same prop is already used for local-only forms such as RulePageBase.tsx, ReportAddApproverPage.tsx, SpendRuleSelectionPage.tsx, SpendRulePageBase.tsx, DynamicExpensifyCardLimitPage.tsx, and AssigneeStep.tsx.
The change is one line and touches no shared components.
What alternative solutions did you explore? (Optional)
Move getSpendRuleErrorMessage() into validate(), so FormProvider aborts before entering the loading state. This is a valid secondary approach. It needs an error key attached to an input that doesn't represent the spend rule, such as the expiration toggle. The error is also already displayed through the dedicated FormHelpMessage. It also leaves any future early return in submit exposed to the same bug. It could be added later as cleanup but isn't needed for this fix.
Return a rejected promise from submit. Rejected because it doesn't work, as explained in the root cause. It would also produce an unhandled promise rejection.
Change the shared hook so onSubmit can return false for "did not submit" and startWithLoading clears isPressed. This would protect all forms, but it changes the contract of FormProvider and usePressLoading for a very large number of forms. That is too much regression risk for a bug that is local to one step.
Reset the press state from handleToggleSpendRules. Rejected because isPressed is internal to FormProvider, and it would only patch this one path.
Testing
Manual:
Open a workspace with the Expensify Card enabled, click Issue card, enter an amount, and press Next.
On "Set card rules", enable Add spend rule and press Next without configuring a rule. Verify that the error appears and the Next button remains a normal, pressable button, not a spinner.
Press Next again and verify the error remains and the button is still interactive.
Disable Add spend rule. Verify Next is still shown as a button, and pressing it advances to the next step (card name).
Regression: enable a rule properly (for example, set a max amount) and press Next. Verify it proceeds normally. Also verify the edit flow, where the button reads Confirm, still works.
Automated (tests/unit/SetSpendRulesStepTest.tsx):
Render the step with the toggle on and no rule set, and press Next. Assert the validation error is visible and the submit button isn't in a loading state.
Toggle off, press Next, and assert setIssueNewCardStepAndData is called with the CARD_NAME step.
📣 @Abdallah-1551! 📣
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:
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.
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.
Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>
Please re-state the problem that we are trying to solve in this issue.
In the Issue Expensify Card flow (Set card rules step), if the user enables Add spend rule, clicks Next without selecting/configuring a rule (which shows a validation error), and then disables Add spend rule, the Next button remains permanently stuck as a disabled loading spinner, preventing the user from continuing.
What is the root cause of that problem?
FormProvider (src/components/Form/FormProvider.tsx:156, 355) defaults shouldShowLoadingImmediatelyOnPress to true. When the submit button is pressed and validate() returns no errors, FormProvider calls startWithLoading(runSubmit), which immediately sets isPressed = true in src/hooks/usePressLoading.ts:35-46 and renders the loading spinner on the Next button.
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:
The validate function (lines 220-241) only validates expiration dates (validFrom / validThru) and does not check whether a spend rule has been configured.
Instead, the spend-rule check is performed inside submit (lines 189-196):
Because validate returned {} (valid), FormProvider already activated isPressed = true (spinner) before calling submit. When submit hits the early return;, nothing resets isPressed in usePressLoading (which only resets when an external isLoading transitions, the screen blurs, or onSubmit throws). Toggling Add spend rule off clears spendRuleErrorMessage, but isPressed stays true forever.
What changes do you think we should make in order to solve the problem?
In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:
Pass shouldShowLoadingImmediatelyOnPress={false} to <FormProvider> on SetSpendRulesStep (since this wizard step only writes draft values to Onyx and navigates synchronously to the next step without making an async network request), or move getSpendRuleErrorMessage() into validate so FormProvider blocks submission before calling startWithLoading(runSubmit).
Moving the check into validate (or setting shouldShowLoadingImmediatelyOnPress={false}) ensures isPressed is never stuck true when spend-rule validation fails.
What alternative solutions did you explore? (Optional)
We also considered updating usePressLoading.ts so startWithLoading resets isPressed in a finally block when the wrapped callback returns synchronously without triggering navigation or isLoading, which is a good general safeguard for synchronous form steps.
@Ollyws Confirmed — this is no longer reproducible on latest main.
It was resolved by commits 99bf35b ("Stop the press spinner from hanging when isLoading never turns true") and 4bed47f in src/hooks/usePressLoading.ts (line 66), which added:
Previously, when SetSpendRulesStep.tsx returned early inside submit() on if (formSpendRuleError) (lines 189-196) without navigating away, isPressed stayed true indefinitely. Now usePressLoading automatically releases isPressed back to false on the next animation frame because the screen remains focused (navigationContext?.isFocused() is true).
(The only remaining nuance in SetSpendRulesStep.tsx is that validating getSpendRuleErrorMessage() inside submit instead of validate briefly triggers startWithLoading for one frame before releasing, unless we move getSpendRuleErrorMessage() into validate — otherwise this issue can be closed as fixed!)
Activity
applause-bot commentedon Oct 20, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/3c4f987dc2da44fc4f2a31d85ee45b51f601b4682593569ac9892921521712aa
MelvinBot commentedon Oct 20, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the Issue card "Set card rules" step, if you turn on Add spend rule, click Next without setting a rule, and then turn the toggle off, the Next button stays a spinner. You can't press it, so you're stuck.
What is the root cause of that problem?
Clicking Next turns on the form's "spinner on press" state. The step's submit handler then stops early because no spend rule is set, so it never moves on. Nothing clears the spinner after that early stop.
FormProvidershows a spinner on press by default (FormProvider.tsx:156). It callsstartWithLoading, which setsisPressed = truebefore it runsonSubmit(FormProvider.tsx:355, usePressLoading.ts:35-46).isPressedis cleared in only three cases: the handler throws, the screen regains focus, or an externalisLoadingturns on. The earlyreturninSetSpendRulesStep'ssubmittriggers none of them (SetSpendRulesStep.tsx:190-196).The spinner-on-press default came from Introduce the spinner on the `FormAlertWithSubmitButton` buttons. The early return in this step is older and was harmless before that change. It is not related to Return to the spend rule form after upgrading from a workspace spend rule page, the PR under QA when this was found.
What changes do you think we should make in order to solve the problem?
Pass
shouldShowLoadingImmediatelyOnPress={false}to theFormProviderinSetSpendRulesStep(SetSpendRulesStep.tsx:273-281). Next here only writes local Onyx data, so the user never waits long enough to need a spinner. Other forms already do this, for exampleRulePageBase.tsxandReportAddApproverPage.tsx.What alternative solutions did you explore? (Optional)
Fix it once in the shared hook: let
onSubmitreturnfalsefor "did not submit," and havestartWithLoadingclearisPressedwhen that happens. This would protect any other form whose submit handler stops early. It touches shared form components, so it's a bigger change than this bug needs.Investigation notes
useFocusEffectinusePressLoadingthen clears the spinner.validatein this step checks only the expiration dates. It passes with the toggle on and no rule set, so the press reachessubmit.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/104509)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Oct 20, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @eh2077 (
External)[-]Expensify Card - Next button replaced with loading button after enabled then disabled rule[/-][+][$175] Expensify Card - Next button replaced with loading button after enabled then disabled rule[/+]melvin-bot commentedon Oct 20, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022112649059231981593
juni2003 commentedon Oct 20, 2026
Contributor details
Your Expensify account email: juni.xatti@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~01867ae3203c9b7815?mp_source=share
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the "Set card rules" step of issuing an Expensify Card, clicking "Next" with the "Add spend rule" toggle ON (but no rule actually configured) leaves the "Next" button permanently stuck in a loading state, even if the user subsequently turns the toggle OFF.
What is the root cause of that problem?
The root cause is an unhandled early return inside the form's
onSubmithandler combined with Expensify'susePressLoadinghook.FormProvider.tsxtriggersstartWithLoading, which immediately sets the button'sisPressedstate totrueto show the spinner.submitinsrc/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsx.onSubmitfunction returnsundefinedinstead of a rejected Promise or throwing an error,usePressLoading.tsassumes the submission was "successful." Since the screen did not unmount or lose focus (which are the only other ways the spinner clears), theisPressedstate is permanently orphaned astrue.What changes do you think we should make in order to solve the problem?
We should not disable the loading spinner (as MelvinBot suggested), as that degrades the standard form UX. Instead, the
onSubmitearly return must inform theFormProviderthat the submission was aborted so theusePressLoadinghook naturally clears the spinner.In
src/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsx, update thesubmitfunction to return a rejected Promise when the spend rule guard is hit:Because
FormProviderwraps theonSubmitcall in atry/catchand.catch()chain, rejecting the promise natively resolves the stuck spinner while maintaining the required UX loading state for successful submissions.What alternative solutions did you explore? (Optional)
MelvinBot suggested passing
shouldShowLoadingImmediatelyOnPress={false}to theFormProvider. This was rejected because it removes visual feedback from the button entirely. WhileIssueNewCardFlowOnyx writes are fast, standardizing on no-spinner forms creates UX inconsistency across the app. Rejecting the Promise fixes the root cause at the state-machine level without compromising the UI feedback loop.melvin-bot commentedon Oct 20, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
camesenin commentedon Oct 20, 2026
Contributor details
Your Expensify account email: camesenin@gmail.com
Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604
Proposal
Please re-state the problem that we are trying to solve in this issue.
In the Issue Expensify Card flow ("Set card rules" step), enabling Add spend rule, clicking Next without configuring a rule, and then turning the toggle off leaves the Next button permanently locked in a loading spinner state, preventing progression.
What is the root cause of that problem?
In
src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:FormProviderusesusePressLoadingwithshouldShowLoadingImmediatelyOnPress={true}by default (src/components/Form/FormProvider.tsx:156, 355).startWithLoadingsynchronously setsisPressed = true, rendering the loading spinner on the button.SetSpendRulesStep.tsx:190-196:submit()executes an earlyreturn.isPressedinusePressLoadingonly resets when an async promise finishes, the handler throws, or the screen loses/regains focus. An earlyreturnleavesisPressed = truepermanently.SetSpendRulesStep.tsx:111-117, toggling Add spend rule off only callssetCustomErrorMessage(''), but never resetsisPressed, leaving the button permanently locked as a spinner.What changes do you think we should make in order to solve the problem?
Disable immediate press loading on
SetSpendRulesStep.tsx:In
src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:273-281:Pass
shouldShowLoadingImmediatelyOnPress={false}toFormProvider:RulePageBase.tsxandReportAddApproverPage.tsx) follow this exact pattern.Clean error and state reset on toggle change:
In
SetSpendRulesStep.tsx:111-117, resethasSpendRulesand explicitly clear local validation errors when toggled off.Proof of Concept (Reproduction Demo)
What specific scenarios should we cover in automated tests?
In
tests/unit/SetSpendRulesStepTest.tsx:SetSpendRulesStepwith toggle ON, submit without selecting rules, assert validation error appears and the submit button remains interactive (not locked in a spinner).melvin-bot commentedon Oct 20, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
khizzcr7 commentedon Oct 21, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the Issue card "Set card rules" step, turning on "Add spend rule", clicking Next without configuring a rule, then turning the toggle off leaves the Next button permanently stuck in a loading spinner state, blocking progression.
What is the root cause of that problem?
FormProvidershows a spinner on press by default (shouldShowLoadingImmediatelyOnPress = true,src/components/Form/FormProvider.tsx:156).startWithLoadingsetsisPressed = truebefore runningonSubmit(FormProvider.tsx:355).isPressedis cleared in only three cases: the handler throws, the screen regains focus, or an externalisLoadingturns on.src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx,submitearly-returns when no rule is configured:returntriggers none of the three reset paths, so the spinner never clears. Turning the toggle off only clears the error text (SetSpendRulesStep.tsx:111-117); later presses are ignored (FormProvider.tsx:311).FormAlertWithSubmitButtonbuttons #92498; the early return predates it and was harmless before. The PR under QA (Return to the spend rule form after upgrading from a workspace spend rule page #103365) is unrelated.What changes do you think we should make in order to solve the problem?
Pass
shouldShowLoadingImmediatelyOnPress={false}to theFormProviderinSetSpendRulesStep(lines 273-281). Next here only writes local Onyx data, so the user never waits long enough to need a spinner; precedent exists inRulePageBase.tsxandReportAddApproverPage.tsx. This removes the stuck-spinner state for every early-return path rather than patching each one. I will re-verify the full toggle on -> Next -> error -> toggle off -> Next flow after the change.Tests: submit with the toggle on and no rule set, assert the button is not left in a loading state after the error appears; turn the toggle off, press Next, assert navigation proceeds.
7 remaining items
Abdallah-1551 commentedon Oct 22, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the Issue Expensify Card "Set card rules" step, the user turns on Add spend rule, presses Next without configuring a rule, and sees the validation error. If they then turn the toggle off, the Next button stays replaced by a loading spinner. It can't be pressed again, so the user is stuck and can't continue the card issuing flow.
What is the root cause of that problem?
FormProvidershows a spinner as soon as the submit button is pressed. Nothing clears that spinner when the step'ssubmithandler stops early.FormProviderdefaultsshouldShowLoadingImmediatelyOnPresstotrue(FormProvider.tsx:156). On press it callsstartWithLoading(runSubmit)(FormProvider.tsx:355). That setsisPressed = trueinusePressLoadingbeforeonSubmitruns, so the button renders a spinner.SetSpendRulesStep.validateonly checks the expiration dates. With the toggle on and no rule set, it returns no errors, so the press goes through tosubmit.SetSpendRulesStep.tsx:190-196,submitruns an ad-hoc check and returns early:isPressedis only cleared in these cases: the handler throws synchronously, the screen regains focus (useFocusEffect), or an externalisLoadingflips on. The earlyreturntriggers none of them, soisPressedstaystrue.handleToggleSpendRules, lines 111-117) only clears the error text. It never touchesFormProvider's press state. The button stays a spinner, andFormProviderdrops later presses because of itsif (isLoading) return;guard (FormProvider.tsx:~311).The spinner-on-press default came from PR #92498. The early return predates it and was harmless until then. PR #103365, the one under QA when this was found, is unrelated.
The
Promise.rejectsuggestion would not work. With the defaultDISMISS_THEN_SUBMITkeyboard behavior,onSubmitis invoked insideKeyboardUtils.dismiss().then(...).startWithLoadingdoesn't chain that promise, so a rejection would be unhandled and would not clearisPressed.What changes do you think we should make in order to solve the problem?
In
src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx, passshouldShowLoadingImmediatelyOnPress={false}to theFormProvider(around lines 273-281):<FormProvider style={[styles.flexGrow1, styles.ph5]} formID={ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM} onSubmit={submit} submitButtonText={isEditing ? translate('common.confirm') : translate('common.next')} enabledWhenOffline shouldHideFixErrorsAlert validate={validate} + shouldShowLoadingImmediatelyOnPress={false} >Why this is the right fix:
submitonly writes local draft data to Onyx (setIssueNewCardStepAndData) and navigates to the next step. No network call happens, so an immediate press spinner adds nothing.FormProvidernever enters the press-loading state for this form. This fixes the reported flow and any other early exit insubmit, with no per-path cleanup.RulePageBase.tsx,ReportAddApproverPage.tsx,SpendRuleSelectionPage.tsx,SpendRulePageBase.tsx,DynamicExpensifyCardLimitPage.tsx, andAssigneeStep.tsx.The change is one line and touches no shared components.
What alternative solutions did you explore? (Optional)
getSpendRuleErrorMessage()intovalidate(), soFormProvideraborts before entering the loading state. This is a valid secondary approach. It needs an error key attached to an input that doesn't represent the spend rule, such as the expiration toggle. The error is also already displayed through the dedicatedFormHelpMessage. It also leaves any future early return insubmitexposed to the same bug. It could be added later as cleanup but isn't needed for this fix.submit. Rejected because it doesn't work, as explained in the root cause. It would also produce an unhandled promise rejection.onSubmitcan returnfalsefor "did not submit" andstartWithLoadingclearsisPressed. This would protect all forms, but it changes the contract ofFormProviderandusePressLoadingfor a very large number of forms. That is too much regression risk for a bug that is local to one step.handleToggleSpendRules. Rejected becauseisPressedis internal toFormProvider, and it would only patch this one path.Testing
Manual:
Automated (
tests/unit/SetSpendRulesStepTest.tsx):setIssueNewCardStepAndDatais called with theCARD_NAMEstep.melvin-bot commentedon Oct 22, 2026
📣 @Abdallah-1551! 📣
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:
Format:
melvin-bot commentedon Oct 23, 2026
@eh2077 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
melvin-bot commentedon Oct 27, 2026
@eh2077 Still overdue 6 days?! Let's take care of this!
eh2077 commentedon Oct 28, 2026
reassign issue cuz I'll be ooo
melvin-bot commentedon Oct 28, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @Ollyws (
External)Kabeer099 commentedon Oct 29, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In the Issue Expensify Card flow (Set card rules step), if the user enables Add spend rule, clicks Next without selecting/configuring a rule (which shows a validation error), and then disables Add spend rule, the Next button remains permanently stuck as a disabled loading spinner, preventing the user from continuing.
What is the root cause of that problem?
FormProvider(src/components/Form/FormProvider.tsx:156, 355) defaultsshouldShowLoadingImmediatelyOnPresstotrue. When the submit button is pressed andvalidate()returns no errors,FormProvidercallsstartWithLoading(runSubmit), which immediately setsisPressed = trueinsrc/hooks/usePressLoading.ts:35-46and renders the loading spinner on the Next button.src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:validatefunction (lines 220-241) only validates expiration dates (validFrom/validThru) and does not check whether a spend rule has been configured.submit(lines 189-196):validatereturned{}(valid),FormProvideralready activatedisPressed = true(spinner) before callingsubmit. Whensubmithits the earlyreturn;, nothing resetsisPressedinusePressLoading(which only resets when an externalisLoadingtransitions, the screen blurs, oronSubmitthrows). Toggling Add spend rule off clearsspendRuleErrorMessage, butisPressedstaystrueforever.What changes do you think we should make in order to solve the problem?
In
src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:shouldShowLoadingImmediatelyOnPress={false}to<FormProvider>onSetSpendRulesStep(since this wizard step only writes draft values to Onyx and navigates synchronously to the next step without making an async network request), or movegetSpendRuleErrorMessage()intovalidatesoFormProviderblocks submission before callingstartWithLoading(runSubmit).validate(or settingshouldShowLoadingImmediatelyOnPress={false}) ensuresisPressedis never stucktruewhen spend-rule validation fails.What alternative solutions did you explore? (Optional)
We also considered updating
usePressLoading.tssostartWithLoadingresetsisPressedin afinallyblock when the wrapped callback returns synchronously without triggering navigation orisLoading, which is a good general safeguard for synchronous form steps.Ollyws commentedon Oct 29, 2026
I can't reproduce on the latest main, think it's been fixed already:
Screen.Recording.2026-10-29.at.18.18.16.mov
@Kabeer099 @khizzcr7 @sijo0218 can you still reproduce?
Kabeer099 commentedon Oct 29, 2026
@Ollyws Confirmed — this is no longer reproducible on latest
main.It was resolved by commits
99bf35b("Stop the press spinner from hanging when isLoading never turns true") and4bed47finsrc/hooks/usePressLoading.ts(line 66), which added:Previously, when
SetSpendRulesStep.tsxreturned early insidesubmit()onif (formSpendRuleError)(lines 189-196) without navigating away,isPressedstayedtrueindefinitely. NowusePressLoadingautomatically releasesisPressedback tofalseon the next animation frame because the screen remains focused (navigationContext?.isFocused()istrue).(The only remaining nuance in
SetSpendRulesStep.tsxis that validatinggetSpendRuleErrorMessage()insidesubmitinstead ofvalidatebriefly triggersstartWithLoadingfor one frame before releasing, unless we movegetSpendRuleErrorMessage()intovalidate— otherwise this issue can be closed as fixed!)