Skip to content

Issue #2 — Faucet: give each token card its own claim state #175

@IbrahimIjai

Description

@IbrahimIjai

Labels: bug, enhancement, good-first-issue, ui/ux, bounty:$100

Summary

On /faucet, clicking Claim on a single token card triggers a global loading state: every card's Claim button and the big "Claim Test Tokens" button all show as pending/disabled simultaneously, even though only one token is being claimed. This makes the page feel broken and prevents the user from understanding what is actually happening.

Each card must own its own claim lifecycle (idle → pending → success/error), independent of the other cards and of the bulk-claim button.

Root cause

useClaim exposes a single boolean isPending for the whole hook:

// apps/web/src/features/faucet/hooks/useClaim.tsx
const [isPending, setIsPending] = useState(false)
const claim = useCallback(async (tokenIds = FAUCET_TOKENS.map(t => t.contractId)) => {
  setIsPending(true)
  ...
}, [...])
return { claim, isPending }

faucet-page.tsx then fans that one flag out to every card and to the bulk button:

const { claim, isPending } = useClaim()
const claimDisabled = !isConnected || isPending || mismatch   // isPending = ANY claim in flight
...
{FAUCET_TOKENS.map((token) => (
  <TokenCard ... isPending={isPending} isDisabled={claimDisabled}
             onClaim={(t) => claim([t.contractId])} />
))}
...
<Button disabled={claimDisabled} onClick={() => claim()}>  {/* bulk */}
  {isPending ? "Claiming…" : "Claim Test Tokens"}
</Button>

So a single-token claim and the bulk claim are indistinguishable to the UI.

Acceptance criteria

  • Clicking Claim on one card shows the pending state on only that card — its button spins/disables; other cards stay interactive.
  • The bulk "Claim Test Tokens" button shows pending only when the bulk claim is running, not when an individual card is claiming.
  • While a card is claiming, that card's button is the only one disabled for that reason (the global !isConnected / mismatch disables still apply to all).
  • Per-card success and error feedback: a failed claim on one card surfaces its error on that card (or via the existing toast) without flipping every other card into an error/pending state. Keep the existing ClaimTooSoon cooldown messaging.
  • On success, balances refresh (the existing invalidateQueries(queryKeys.faucet.data(address)) behaviour is preserved) and the just-claimed card reflects the new balance / updated "Last claim ledger".
  • Prevent double-submits: a card already in-flight ignores repeat clicks; the bulk button is disabled while any/all relevant claims are in flight (define and document the rule — recommended: bulk disabled while bulk is running; individual cards independent).
  • No regression to the toast flow, explorer link, or cooldown copy.

Suggested approach (non-binding)

Track which token(s) are in flight instead of a single boolean — e.g. useClaim returns pendingTokens: Set<string> (or a pendingId and an isBulkPending), and the page derives each card's isPending from membership. Keep a clear distinction between a single-token claim and the bulk claim() with no args. A small state machine or a Record<contractId, status> is also fine. Whatever the shape, the card components should remain dumb and receive their own isPending / status as props (TokenCard already takes per-card props — extend that pattern; the local state lives in the page or hook, not scattered into each card via separate hook instances).

Avoid the trap of calling useClaim() once per card to "isolate" state — that creates N independent toast/submit pipelines and N wallet-kit references. Prefer one hook that tracks per-token status.

Out of scope

  • Changing the underlying claim_many contract call or the cooldown logic.
  • Faucet visual redesign (cards already look good) — this is a state/UX correctness fix.

Pointers

Definition of done

  • Single-card and bulk claims are visually independent; bun run typecheck and bun run lint pass; manually verified on testnet with a connected wallet (claim one token, confirm other cards/bulk button stay idle; claim bulk, confirm cards reflect it). Before/after screen recording or GIF in the PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official Campaign

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions