Skip to content

feat: per-wallet buy cooldown countdown display on key detail page (#915) - #1045

Closed
meem08 wants to merge 4 commits into
accesslayerorg:devfrom
meem08:feat/915-per-wallet-buy-cooldown-countdown
Closed

meem08 wants to merge 4 commits into
accesslayerorg:devfrom
meem08:feat/915-per-wallet-buy-cooldown-countdown

Conversation

@meem08

@meem08 meem08 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Closes #915

Summary

The creator key detail page now shows a live per-wallet buy cooldown countdown and locks the Buy Key button until the connected wallet's cooldown expires.

Today the page renders a BuyCooldownCountdown chip sourced from whatever nextBuyAllowedAt happens to be on the caller's held position, but nothing gates the buy action - the button stays fully enabled during a cooldown, so a user can open the trade dialog and get as far as signing a transaction the contract will reject. The countdown is also rendered outside the buy panel, disconnected from the action it is supposed to gate.

This PR makes the cooldown a real, enforced gate:

  1. A dedicated read for the connected wallet's cooldown expiry, rather than inferring it from a holdings payload.
  2. The countdown moved into the buy panel, next to the button it gates.
  3. The Buy button is disabled while a cooldown is active, shows the remaining time, and is linked to the lockout reason via aria-describedby.
  4. The button re-enables automatically on the tick the countdown crosses zero - no refetch, no reload.
  5. No cooldown means no cooldown UI: the normal buy panel, no timer, no extra copy.

Changes

New - cooldown read

File What
src/services/course.service.ts KeyBuyCooldown type + getKeyBuyCooldown(keyId, wallet) ? GET /keys/:keyId/buy-cooldown?wallet=.
src/hooks/useKeyBuyCooldown.ts TanStack Query read hook over that endpoint
src/lib/queryKeys.ts queryKeys.creators.buyCooldown(creatorId, wallet) - wallet-scoped key

The query is enabled: Boolean(keyId && wallet), which is what gives both required triggers for free:

  • key page load - fires as soon as the key and a connected wallet are both known;
  • wallet connect - flipping enabled from false to true mounts the query mid-session, so connecting a wallet while the key page is open triggers the read without a reload.

Because the cache key includes the wallet, switching wallets cannot leak one wallet's cooldown into another's view.

New - shared countdown clock

File What
src/hooks/useBuyCooldownCountdown.ts Ticking clock returning { remainingSeconds, formattedRemaining, isActive }

The key design point: the timer and the button read from the same source of truth. Both derive from the same absolute nextBuyAllowedAt, so the chip and the button can never disagree, and isActive flips to false on the exact tick that crosses zero - which is precisely what re-enables the button.

Other properties worth noting:

  • The interval only runs while a cooldown is actually in effect, so a key with no cooldown costs zero timers.
  • onExpire fires once per expiry and is held in a ref, so passing an inline callback does not restart the interval.
  • BuyCooldownCountdown was refactored onto this hook; its props, data-testids, role="status", and aria-label are unchanged, so it stays usable standalone.

Changed - key detail page

src/pages/CreatorDetailPage.tsx:

  • The dedicated read is now the source of truth for the cooldown, with the held-position value (Add a buy cooldown countdown on the key detail page showing when the authenticated user can next buy #873) and the creator-wide fallback retained behind it so the countdown still works against backends that have not shipped the endpoint.
  • The countdown chip moved inside the buy panel, under the spread/oracle indicators.
  • The Buy button is disabled while a cooldown is active, switches to the outline variant, and its label becomes Buy in 4m 32s so the remaining time is visible on the action itself (mirroring the existing Buy Disabled (Deprecated) pattern). A supporting Unlocks in . line is rendered as the button's aria-describedby target.
  • At expiry the cooldown is re-read. This matters: a configured cooldown means the next buy opens a fresh window, so a stale "no cooldown" answer must not be trusted for long.
  • The panel subtitle switches to Buy cooldown active for your wallet. while locked.

Behaviour matrix

State Timer Buy button
Wallet in cooldown Visible, ticking, inside buy panel Disabled, labelled Buy in <remaining>
Cooldown expires Unmounts, reason text clears Re-enables automatically, label returns to Buy Key
No cooldown (null / 404 / absent) Not rendered Normal Buy Key
Cooldown read fails Not rendered Normal Buy Key - a failed read never blocks buying
No wallet connected Not rendered (read is not issued) Normal Buy Key
Key deprecated Not rendered Unchanged Buy Disabled (Deprecated)

A read failure deliberately does not gate the buy button. retry: false plus a 404?null mapping means an absent endpoint degrades to today's behaviour rather than locking every user out of a key.

Tests

23 new tests, all passing:

  • src/hooks/__tests__/useKeyBuyCooldown.test.tsx - reads for the connected wallet; null is a successful "no cooldown" rather than an error; does not fetch until a wallet is connected, then fetches on connect; a wallet switch re-reads instead of reusing the previous cache entry; no fetch without a key id.
  • src/hooks/__tests__/useBuyCooldownCountdown.test.tsx - inactive with no data and no timer allocated; accepts seconds/ms epochs; ticks down; deactivates on the tick that crosses zero; onExpire fires exactly once and stops the timer; an already-expired cooldown never fires onExpire; an inline callback does not restart the interval; recomputes on a new expiry.
  • src/pages/__tests__/CreatorDetailPage.buyCooldown.integration.test.tsx - end-to-end coverage of every acceptance criterion, driving the real query layer and only stubbing the service call: fetch on load, fetch on connect, timer + disabled button while locked, disabled button cannot open the trade dialog, auto re-enable at expiry followed by a working buy, re-read at expiry, and the three no-cooldown cases (null, empty, read failure).

Verification

  • pnpm lint - clean.
  • pnpm build (tsc -b + vite build) - clean.
  • Full suite compared against a stashed baseline: no regressions (baseline 132 pre-existing failures ? 131; the one delta is a pre-existing flaky landing-page-adjacent test that this run happened to pass). All 7 CreatorDetailPage suites and all 5 cooldown-related suites pass in full.

Note on the "from contract" wording

The issue asks for the expiry to be read "from contract". This repo has no client-side contract-read infrastructure for creator keys - every key-detail read (#951 config/spread, #967 oracle, #975 performance bond, #960 vesting) goes through courseService, with the backend reading the contract. The new endpoint follows that same convention: it reads the creator's set_buy_cooldown window and the caller's last-buy ledger on-chain, and the client never talks to a contract client directly. This is also the shape the dashboard's existing cooldown panel (#889) assumes.

Design notes

  • isActive over remaining > 0 scattered in the component. The alternative - letting the component own the timer and lifting only a boolean up - was rejected because the two would then be independent tickers. One hook, one clock.
  • Re-read on expiry rather than a fixed poll. A refetchInterval would keep the endpoint hot for every visitor even when no cooldown exists. Re-reading on the single meaningful transition costs one request per cooldown, and only for wallets that actually have one.
  • aria-describedby over a tooltip. The lockout reason is state a screen-reader user must not miss; a tooltip is hover-gated and would leave the disabled button unexplained.

…erorg#915)

Adds a dedicated read for the connected wallet's buy-cooldown expiry, a shared
ticking countdown hook, and a locked-out Buy button state on the creator key
detail page.

Scope:
- KeyBuyCooldown + courseService.getKeyBuyCooldown (GET
  /keys/:keyId/buy-cooldown) and the useKeyBuyCooldown query hook, disabled until
  a wallet is connected so the read fires on key page load and again on wallet
  connect
- useBuyCooldownCountdown, the single source of truth for remaining cooldown
  time, so the timer and the Buy button can never disagree; BuyCooldownCountdown
  now builds on it
- Buy panel shows the countdown in-panel, disables the button, surfaces the
  remaining time on the button and in an aria-describedby hint, re-enables on the
  tick that crosses zero, and re-reads the cooldown at expiry
- No cooldown (null/404/absent) leaves the normal Buy UI with no timer; a failed
  read never blocks buying

Refs accesslayerorg#915
@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@meem08 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

@Chucks1093

Copy link
Copy Markdown
Member

Conflict

meem08 and others added 3 commits September 28, 2026 12:50
…rg#915)

The merge of dev into this branch left two malformed sections:

- CreatorDetailPage: the buy-panel JSX was duplicated by the merge
  (PR's cooldown-aware button plus dev's TradeCooldownButton), leaving a
  mangled subtitle ternary and stray closing tags.
- course.service.ts: the getKeyBuyCooldown catch block was truncated and
  getTradeCooldownStatus was spliced into its body.

Reconcile the buy panel into a single button that layers the states:
deprecated -> whitelist lockout -> per-wallet buy cooldown (accesslayerorg#915) ->
TradeCooldownButton (accesslayerorg#998). Restore the getKeyBuyCooldown method body.
…y-cooldown-countdown

# Conflicts:
#	src/pages/CreatorDetailPage.tsx
@Chucks1093 Chucks1093 closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement per-wallet buy cooldown countdown display on key detail page

2 participants