Skip to content

MilestoneFundButton has no amount input, making partial milestone funding impossible from the UI #83

Description

@chonilius

Overview

src/app/milestones/MilestoneActions.tsx defines two sibling "fund something" components back to back, and only one of them lets the user say how much:

export function MilestoneFundButton({ milestoneId }: { milestoneId: string }) {
  const router = useRouter();
  const { address, connect, connecting } = useWallet();
  const [pending, setPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function handleFund() {
    setError(null);
    setPending(true);
    try {
      const walletAddress = address ?? (await connect());
      if (!walletAddress) {
        setError("Connect a Stellar wallet to fund this milestone.");
        return;
      }
      await apiPost(`/milestones/${milestoneId}/fund`, {
        funderAddress: walletAddress,
      });
      router.refresh();
    } catch (err) { ... }
    finally { setPending(false); }
  }

  return (
    <div className="mt-4">
      <Button size="sm" variant="outline" onClick={handleFund} disabled={pending || connecting}>
        {pending || connecting ? "Confirming in wallet..." : "Fund milestone"}
      </Button>
      {error && <p className="mt-2 text-xs text-rose-600">{error}</p>}
    </div>
  );
}

handleFund posts { funderAddress: walletAddress } — no amount field at all. Compare this to PoolDepositButton, defined a few lines later in the exact same file, for the conceptually near-identical action of depositing into a maintenance pool:

export function PoolDepositButton({ poolId }: { poolId: string }) {
  ...
  const [amount, setAmount] = useState("100");
  async function handleDeposit() {
    ...
    await apiPost(`/maintenance-pools/${poolId}/deposit`, {
      amount,
      funderAddress: walletAddress,
    });
    ...
  }
  return (
    <div className="mt-4 flex items-center gap-2">
      <input type="number" min="1" value={amount} onChange={(e) => setAmount(e.target.value)} ... />
      <Button ...>{pending || connecting ? "Confirming..." : "Deposit"}</Button>
      ...

PoolDepositButton correctly collects an amount from the user before posting. MilestoneFundButton collects nothing.

This is a real functional gap, not just a stylistic inconsistency, given how MilestonesPage itself describes and visualizes milestone funding: each milestone renders a progress bar showing distributed out of budget, explicitly modeling partial, incremental funding over time — "Sponsors can fund an entire release instead of a single issue. Budget is distributed automatically across the milestone's issues as each one resolves" (README) and "Sponsors deposit monthly so maintainers can reward ongoing upkeep" for the adjacent maintenance-pool section on the very same page. A progress bar that's meaningfully partway filled (e.g. m2's mock data: distributed: 2100 of budget: 6000) only makes sense if multiple funding contributions, of varying amounts, are expected over the milestone's lifetime — yet the one button that triggers a milestone-funding action provides no way to specify how much of that budget gap this particular contribution should cover.

Either the backend endpoint (POST /milestones/:id/fund) silently interprets a missing amount as "fund the entire remaining budget in one shot" (in which case the UI is misleadingly labeled — a button with no amount input reads as "fund this milestone," not "fund the entire remaining budget," and a sponsor clicking it with no amount field visible has no way to know or control that), or the endpoint requires an amount and this call is simply broken/incomplete and would fail or behave unpredictably against a real backend. Either way, this is worth resolving explicitly rather than left as an unexplained asymmetry with its sibling component two components down in the same file.

Requirements

  • Add an amount input to MilestoneFundButton, matching the pattern already established by PoolDepositButton in the same file (numeric input, validated per the separate amount-validation issue in this batch that also covers PoolDepositButton's current lack of validation — apply the same validation to this new input from the start rather than introducing a second unvalidated amount field).
  • Confirm with the actual mergefi-backend contract (or by testing against a running instance) what POST /milestones/:id/fund currently expects — if it already requires an amount and this frontend call has simply never sent one, this is a currently-broken/non-functional action end to end, which materially raises the priority of this fix. Document the finding in the PR.
  • Consider whether the amount should be capped/defaulted sensibly relative to the milestone's remaining budget (budget - distributed) — e.g. pre-filling or capping the input so a sponsor can't accidentally attempt to overfund past the stated budget, mirroring the divide-by-zero/overspend-display issue elsewhere in this batch for MilestonesPage's own progress-bar math.

Acceptance Criteria

  • MilestoneFundButton renders an amount input before its "Fund milestone" action, consistent with PoolDepositButton's existing pattern in the same file.
  • handleFund sends the user-entered amount to POST /milestones/:id/fund alongside funderAddress.
  • The input is validated using the same rules established for PoolDepositButton's amount field elsewhere in this batch (no empty/zero/negative/non-numeric/over-precision values reaching the backend).
  • The PR documents what the actual backend contract for this endpoint expects/expected, resolving the "was this ever functional" question raised above.
  • No regression to milestones that are being funded for the first time (a milestone with distributed: 0).

Additional Notes

Precise references:

  • src/app/milestones/MilestoneActions.tsx:9-43 — the full MilestoneFundButton component, handleFund at lines 15-33, no amount state or input anywhere in the component.
  • src/app/milestones/MilestoneActions.tsx:45-88PoolDepositButton, the sibling component in the same file with the correct amount-collection pattern this issue asks to mirror.
  • src/app/milestones/page.tsx:28-58 — the milestone card rendering, including the progress bar (m.distributed / m.budget) that establishes incremental/partial funding as the expected mental model for this feature, and :54 where <MilestoneFundButton milestoneId={m.id} /> is rendered with no budget/distributed context passed in at all (worth passing m.budget - m.distributed through as a prop if a remaining-budget cap/default is implemented).
  • src/lib/mock-data.ts:93-114mockMilestones, showing both m1 and m2 with distributed meaningfully less than budget, reinforcing that partial/incremental funding is the modeled, expected state, not an edge case.

Relationship to other issues: this issue's amount-validation requirement should reuse whatever comes out of the separate PoolDepositButton amount-validation issue in this batch, rather than duplicating that work independently — sequence or land them together given they touch the same file and the same underlying validation concern.

Test/reproduction plan: render MilestoneFundButton, assert an amount input is present and its value flows into the apiPost call's body when "Fund milestone" is clicked; assert the same invalid-input cases (empty, zero, negative, non-numeric) required of PoolDepositButton are also rejected here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingvery hardVery difficult task, expert-level effort required

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions