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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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