Skip to content

[$175] Expensify Card - Next button replaced with loading button after enabled then disabled rule #104509

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


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

Action Performed:

  1. Open staging.new.expensify and login with applausetester@applause.expensifail.com
  2. Go to workspace which has expensify card enable
  3. Click on issue card
  4. Enter amount > next
  5. Enable Add spend rule
  6. Click on next
  7. Disable Add spend rule

Expected Result:

Next button should reappear after disable Add spend rule

Actual Result:

Next button replaced with loading button

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7291385_1792528396777.Recording__1249.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7291385

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @Ollyws

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 20, 2026
  2. applause-bot commented on Oct 20, 2026

    @applause-bot
    Author

    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

  3. MelvinBot commented on Oct 20, 2026

    @MelvinBot
    Contributor

    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.

    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 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.


    view run

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

    @melvin-bot

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

  6. 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
  7. melvin-bot commented on Oct 20, 2026

    @melvin-bot
  8. juni2003 commented on Oct 20, 2026

    @juni2003

    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 onSubmit handler combined with Expensify's usePressLoading hook.

    1. When the user clicks "Next", FormProvider.tsx triggers startWithLoading, which immediately sets the button's isPressed state to true to show the spinner.
    2. The execution passes to submit in src/pages/workspace/expensifyCard/issueNew/SetSpendRulesStep.tsx.
    3. Because the toggle is ON but no limit is set, lines 190-196 trigger an early return:
    if (hasSpendRule && !spendRule?.amount) {
        setRuleError(true);
        return;
    }
    1. 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.tsx
    const submit = () => {
        if (hasSpendRule && !spendRule?.amount) {
            setRuleError(true);
            return Promise.reject(new Error('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.

  9. melvin-bot commented on Oct 20, 2026

    @melvin-bot

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

  10. camesenin commented on Oct 20, 2026

    @camesenin

    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:

    1. FormProvider uses usePressLoading with shouldShowLoadingImmediatelyOnPress={true} by default (src/components/Form/FormProvider.tsx:156, 355).
    2. When the user taps Next, startWithLoading synchronously sets isPressed = true, rendering the loading spinner on the button.
    3. In SetSpendRulesStep.tsx:190-196:
      if (hasSpendRules && !hasAnySpendRules) {
          setCustomErrorMessage(translate('workspace.card.issueNew.spendRules.error.selectAtLeastOneRule'));
          return;
      }
      When no rules are selected, submit() executes an early return.
    4. 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.
    5. 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?

    1. Disable immediate press loading on SetSpendRulesStep.tsx:
      In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx:273-281:
      Pass shouldShowLoadingImmediatelyOnPress={false} to FormProvider:

      <FormProvider
          formID={ONYXKEYS.FORMS.EXPENSIFY_CARD_SPEND_RULES_FORM}
          submitButtonText={translate('common.next')}
          onSubmit={submit}
          validate={validate}
          shouldShowLoadingImmediatelyOnPress={false}
          style={[styles.flex1, styles.mh5]}
          enabledWhenOffline
      >
      • 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.
    2. Clean error and state reset on toggle change:
      In SetSpendRulesStep.tsx:111-117, reset hasSpendRules and 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:

    • 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.
  11. melvin-bot commented on Oct 20, 2026

    @melvin-bot

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

  12. khizzcr7 commented on Oct 21, 2026

    @khizzcr7

    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?

    1. 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).
    2. isPressed is cleared in only three cases: the handler throws, the screen regains focus, or an external isLoading turns on.
    3. In src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx, submit early-returns when no rule is configured:
      const formSpendRuleError = getSpendRuleErrorMessage();
      if (formSpendRuleError) {
          setSpendRuleErrorMessage(formSpendRuleError);
          return;
      }
      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).
    4. The spinner-on-press default came from PR Introduce the spinner on the FormAlertWithSubmitButton buttons #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 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.

  13. 7 remaining items

  14. Abdallah-1551 commented on Oct 22, 2026

    @Abdallah-1551

    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?

    FormProvider shows a spinner as soon as the submit button is pressed. Nothing clears that spinner when the step's submit handler stops early.

    1. 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.
    2. 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.
    3. In SetSpendRulesStep.tsx:190-196, submit runs an ad-hoc check and returns early:
      const formSpendRuleError = getSpendRuleErrorMessage();
      if (formSpendRuleError) {
          setSpendRuleErrorMessage(formSpendRuleError);
          return;
      }
    4. 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.
    5. 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):

     <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:

    • 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:

    1. Open a workspace with the Expensify Card enabled, click Issue card, enter an amount, and press Next.
    2. 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.
    3. Press Next again and verify the error remains and the button is still interactive.
    4. Disable Add spend rule. Verify Next is still shown as a button, and pressing it advances to the next step (card name).
    5. 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.
  15. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    📣 @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:

    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>
    
  16. melvin-bot commented on Oct 23, 2026

    @melvin-bot

    @eh2077 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  17. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    @eh2077 Still overdue 6 days?! Let's take care of this!

  18. removed their assignment
    on Oct 28, 2026
  19. eh2077 commented on Oct 28, 2026

    @eh2077
    Contributor

    reassign issue cuz I'll be ooo

  20. added
    ExternalAdded to denote the issue can be worked on by a contributor
    and removed
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 28, 2026
  21. melvin-bot commented on Oct 28, 2026

    @melvin-bot

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

  22. Kabeer099 commented on Oct 29, 2026

    @Kabeer099

    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?

    1. 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.
    2. 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):
        const formSpendRuleError = getSpendRuleErrorMessage();
        if (formSpendRuleError) {
            setSpendRuleErrorMessage(formSpendRuleError);
            return;
        }
    3. 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:

    1. 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).
    2. 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.

  23. Ollyws commented on Oct 29, 2026

    @Ollyws
    Contributor

    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?

  24. Kabeer099 commented on Oct 29, 2026

    @Kabeer099

    @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:

    requestAnimationFrame(() => release(press, navigationContext?.isFocused() ?? true));

    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!)

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

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions