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
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-88 — PoolDepositButton, 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-114 — mockMilestones, 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.
Overview
src/app/milestones/MilestoneActions.tsxdefines two sibling "fund something" components back to back, and only one of them lets the user say how much:handleFundposts{ funderAddress: walletAddress }— noamountfield at all. Compare this toPoolDepositButton, defined a few lines later in the exact same file, for the conceptually near-identical action of depositing into a maintenance pool:PoolDepositButtoncorrectly collects an amount from the user before posting.MilestoneFundButtoncollects nothing.This is a real functional gap, not just a stylistic inconsistency, given how
MilestonesPageitself describes and visualizes milestone funding: each milestone renders a progress bar showingdistributedout ofbudget, 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: 2100ofbudget: 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 missingamountas "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
MilestoneFundButton, matching the pattern already established byPoolDepositButtonin the same file (numeric input, validated per the separate amount-validation issue in this batch that also coversPoolDepositButton's current lack of validation — apply the same validation to this new input from the start rather than introducing a second unvalidated amount field).mergefi-backendcontract (or by testing against a running instance) whatPOST /milestones/:id/fundcurrently expects — if it already requires anamountand 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.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 forMilestonesPage's own progress-bar math.Acceptance Criteria
MilestoneFundButtonrenders an amount input before its "Fund milestone" action, consistent withPoolDepositButton's existing pattern in the same file.handleFundsends the user-entered amount toPOST /milestones/:id/fundalongsidefunderAddress.PoolDepositButton's amount field elsewhere in this batch (no empty/zero/negative/non-numeric/over-precision values reaching the backend).distributed: 0).Additional Notes
Precise references:
src/app/milestones/MilestoneActions.tsx:9-43— the fullMilestoneFundButtoncomponent,handleFundat lines 15-33, no amount state or input anywhere in the component.src/app/milestones/MilestoneActions.tsx:45-88—PoolDepositButton, 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:54where<MilestoneFundButton milestoneId={m.id} />is rendered with nobudget/distributedcontext passed in at all (worth passingm.budget - m.distributedthrough as a prop if a remaining-budget cap/default is implemented).src/lib/mock-data.ts:93-114—mockMilestones, showing bothm1andm2withdistributedmeaningfully less thanbudget, 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
PoolDepositButtonamount-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 theapiPostcall's body when "Fund milestone" is clicked; assert the same invalid-input cases (empty, zero, negative, non-numeric) required ofPoolDepositButtonare also rejected here.