Skip to content

fix(issues): funder-only refund, payout-wallet claim gate, dashboard … - #566

Merged
chonilius merged 2 commits into
MergeFi:mainfrom
Mikey-222:fix/issue-actions-auth-claim-wallet-dashboard-races
Sep 27, 2026
Merged

chonilius merged 2 commits into
MergeFi:mainfrom
Mikey-222:fix/issue-actions-auth-claim-wallet-dashboard-races

Conversation

@Mikey-222

@Mikey-222 Mikey-222 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor
  1. A stale networkMismatch blocked every action for the session
  • useWalletAction read the flag once at mount. Fixing the network inside Freighter never cleared it, so every action stayed refused for the rest of the session. It now re-checks live and blocks only if still mismatched.
  • WalletContext exposes recheckNetworkMismatch() for that re-read and returns the current verdict.

ClaimButton result effect (found by the rewritten claim tests)

  • It depended on onClaimSuccess (an inline parent callback) and refetch (rebuilt every render by useBountyStatus), so every parent render re-ran the whole branch: duplicate refetch, duplicate router.refresh(), and a restarted auto-dismiss countdown that could keep the success panel up indefinitely. Both are now read through a ref; the effect fires exactly once per distinct result.

Tests

  • New ContributorDashboardClient.test.tsx: badge honesty (live / mixed / signed-out) and the out-of-order response guard, which fails without the generation check.
  • WalletContext.test.tsx: recheckNetworkMismatch clears a resolved mismatch, keeps a real one, and reports none for an unconnected reader.
  • IssueActions.test.tsx, ClaimButton.test.tsx, useWalletAction.test.tsx and adapters.test.ts updated for the above; claim expectations rewritten for the actual ClaimButton-owned path (Claim Bounty, {} body) instead of the superseded inline one.

En route, pre-existing and blocking next build at HEAD

  • BountyStatusProps had no interval, but LiveBountyStatus passed interval={5000} (TS2322). Exposed the prop with a 5000 default.
  • @testing-library/dom was absent from devDependencies — a peer of @testing-library/react, so the suite did not install clean.

What this PR does

Closes

Closes #

What changed

How to test

Checklist

  • npm run lint passes locally
  • npm test passes locally
  • npm run build passes locally
  • This change is actually reachable/used in the running app
  • I have tested this change in the browser (if UI-related)

…races, stale network mismatch

Four reported frontend bugs on one branch. Verified: eslint clean,
`tsc --noEmit` clean, 627 tests / 41 suites green, `next build`,
verify:headers, verify:env.

1. "Refund sponsor" was offered to every signed-in user

- The funder field existed the whole time: `Bounty.sponsorId` is a plain
  column on the backend entity (unlike `issue`/`claimedBy`, which need an
  explicit `relations` option), so it is serialized in every bounty
  payload — the frontend just dropped it. Added to `Bounty`, `RawBounty`
  and `adaptBounty`.
- `IssueActions` derives `isSponsor` and fails closed: signed-out, or a
  payload with no `sponsorId`, and the button is not rendered.
- Checked the public backend (MergeFi/backend, `BountiesService.refund`):
  it already throws `ForbiddenException` unless `sponsorId ===
  callerUserId`, and the route is JWT + `@Roles(SPONSOR, MAINTAINER)`.
  So this was UI noise that 403'd on click, not an authz hole — but it
  should never have been offered.
- Same file's `fund()` carries the *identical* single-sponsor check, so
  there is no crowdfunding model here to gate: one sponsor, one Escrow.
  Funding stays status-gated only, with a comment recording why, rather
  than inventing a rule the backend does not enforce.
- Refund/fund derive the caller from the JWT, never the request body, so
  no `sponsorId` is sent along.

2. Claiming did not require a payout wallet

- `ClaimButton` now hard-blocks with an amber `role="status"`
  prerequisite panel (with a /connect link) when the signed-in user has
  no `stellarAddress`. That is the durable server-linked field, not the
  live Freighter session: a reader whose extension is locked but whose
  profile holds an address is still claimable.
- Signed-out readers were claiming as nobody; `handleClaim` routes to
  /connect first.
- The server guard is not a safety net: on MergeFi/backend@9c78fd91e,
  `BountiesService.claim()` intends `BadRequestException('... has no
  linked Stellar address')` but interpolates an undefined
  `contributorId`. Team splits are unchecked too — `splitRelease` builds
  `recipientAddress: user?.stellarAddress ?? ''` with no empty test.
  Both recorded in the component as open backend gaps rather than
  assumed fixed.
- New `claimErrorMessage` surfaces non-`ALREADY_CLAIMED` claim failures
  in a red `role="alert"` panel; the race path keeps its 15s notice.

3. Dashboard useEffect races and a dishonest data badge

- Both dashboards keyed their fetch effect on the `user` object, which
  AuthContext re-creates on every `refresh()`, so an unrelated refresh
  restarted the fetch. Deps are now `user?.id` (+ `loading`), and a
  `fetchGeneration` ref drops any response a later run has superseded.
- The contributor badge now correlates the two independent fetches:
  "Live data" only when both reached the backend, "Mixed data" when one
  fell back to the bundled mock rows, "Demo data" signed out. The old
  boolean inferred page-wide liveness from the stats request alone while
  the two bounty sections underneath were mock. Both bounty sections
  also carry their own SampleDataChip where they are rendered.
- The page subtitle is keyed on `user`, not on liveness, so it no longer
  flips mid-flight.

4. A stale `networkMismatch` blocked every action for the session

- `useWalletAction` read the flag once at mount. Fixing the network
  inside Freighter never cleared it, so every action stayed refused for
  the rest of the session. It now re-checks live and blocks only if
  still mismatched.
- `WalletContext` exposes `recheckNetworkMismatch()` for that re-read and
  returns the current verdict.

ClaimButton result effect (found by the rewritten claim tests)

- It depended on `onClaimSuccess` (an inline parent callback) and
  `refetch` (rebuilt every render by `useBountyStatus`), so every parent
  render re-ran the whole branch: duplicate refetch, duplicate
  `router.refresh()`, and a restarted auto-dismiss countdown that could
  keep the success panel up indefinitely. Both are now read through a
  ref; the effect fires exactly once per distinct result.

Tests

- New `ContributorDashboardClient.test.tsx`: badge honesty (live / mixed
  / signed-out) and the out-of-order response guard, which fails without
  the generation check.
- `WalletContext.test.tsx`: `recheckNetworkMismatch` clears a resolved
  mismatch, keeps a real one, and reports none for an unconnected reader.
- `IssueActions.test.tsx`, `ClaimButton.test.tsx`,
  `useWalletAction.test.tsx` and `adapters.test.ts` updated for the
  above; claim expectations rewritten for the actual ClaimButton-owned
  path (`Claim Bounty`, `{}` body) instead of the superseded inline one.

En route, pre-existing and blocking `next build` at HEAD

- `BountyStatusProps` had no `interval`, but `LiveBountyStatus` passed
  `interval={5000}` (TS2322). Exposed the prop with a 5000 default.
- `@testing-library/dom` was absent from devDependencies — a peer of
  `@testing-library/react`, so the suite did not install clean.
@vercel

vercel Bot commented Sep 27, 2026

Copy link
Copy Markdown

@Mikey-222 is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Sep 27, 2026

Copy link
Copy Markdown

@Mikey-222 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@chonilius
chonilius merged commit e03d546 into MergeFi:main Sep 27, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment