TimeCurve Buy button: 'Refreshing quote…' transient label during post-input re-quote (UX) #56

Closed
opened 2026-04-24 02:46:16 +00:00 by Brouie · 9 comments
Brouie commented 2026-04-24 02:46:16 +00:00 (Migrated from gitlab.com)

Summary

On TimeCurvePage, when the user changes the buy-input amount (slider, +/-, or direct typing), the quote is asynchronously re-fetched against the chain while the Buy button remains enabled and labeled 'Buy CHARM'. Rapid input changes followed by clicking Buy within the re-quote window can submit against the stale previous quote — this is the race that the Playwright test in anvil-wallet-writes also hits (see #52).

Proposed

Add a transient visible state during the brief re-quote window:

  • Label changes to something like 'Refreshing quote…' (or equivalent short copy)
  • Button becomes disabled while the quote query is in flight after input change
  • Returns to 'Buy CHARM' + enabled once the fresh quote resolves

This:

  1. Prevents accidental submissions against stale quotes when users click quickly
  2. Makes the async re-fetch state visible (right now it's invisible to users, contributing to the race)
  3. Matches the existing design language of the swap flow which already shows transient quote states

Context

Dev suggested this in #52 follow-up — the test race found there also surfaces a real UX gap the fix should address.

Scope

  • TimeCurvePage Buy button (and any shared buy component if arena + simple share the CTA)
  • Probably a small state flag derived from quote query isFetching after input-change trigger
  • Consider debounce or throttle on the input-change → re-quote trigger as part of the fix (related but optional)

Acceptance criteria

  • Buy button shows 'Refreshing quote…' label + disabled state during re-quote after input change
  • Button returns to normal label + enabled state when fresh quote arrives
  • #52 test race disappears naturally once users can't click during re-quote window
## Summary On `TimeCurvePage`, when the user changes the buy-input amount (slider, +/-, or direct typing), the quote is asynchronously re-fetched against the chain while the Buy button remains enabled and labeled 'Buy CHARM'. Rapid input changes followed by clicking Buy within the re-quote window can submit against the stale previous quote — this is the race that the Playwright test in anvil-wallet-writes also hits (see #52). ## Proposed Add a transient visible state during the brief re-quote window: - Label changes to something like 'Refreshing quote…' (or equivalent short copy) - Button becomes disabled while the quote query is in flight after input change - Returns to 'Buy CHARM' + enabled once the fresh quote resolves This: 1. Prevents accidental submissions against stale quotes when users click quickly 2. Makes the async re-fetch state visible (right now it's invisible to users, contributing to the race) 3. Matches the existing design language of the swap flow which already shows transient quote states ## Context Dev suggested this in #52 follow-up — the test race found there also surfaces a real UX gap the fix should address. ## Scope - `TimeCurvePage` Buy button (and any shared buy component if arena + simple share the CTA) - Probably a small state flag derived from quote query `isFetching` after input-change trigger - Consider debounce or throttle on the input-change → re-quote trigger as part of the fix (related but optional) ## Acceptance criteria - [ ] Buy button shows 'Refreshing quote…' label + disabled state during re-quote after input change - [ ] Button returns to normal label + enabled state when fresh quote arrives - [ ] #52 test race disappears naturally once users can't click during re-quote window
Brouie commented 2026-04-24 02:51:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #52

mentioned in issue #52
PlasticDigits commented 2026-04-24 12:43:38 +00:00 (Migrated from gitlab.com)

mentioned in commit 05c7ebdcfa

mentioned in commit 05c7ebdcfaa7d27e88bb5674c641bbadaad0b21a
PlasticDigits commented 2026-04-24 12:44:15 +00:00 (Migrated from gitlab.com)

Implemented on main (05c7ebd): Kumbaya quoter loading now uses isPending || isFetching so background refetches cannot leave the Simple Buy control enabled with a stale quote after slider/input changes. Primary CTA shows Refreshing quote… while swapQuoteLoading is true (ETH/USDM pay modes). Docs: docs/frontend/timecurve-views.md (Buy quote refresh), docs/testing/invariants-and-business-logic.md (#timecurve-simple-kumbaya-quote-refresh-issue-56), docs/testing/e2e-anvil.md; crosslinks in skills/README.md, skills/play-timecurve-doubloon/SKILL.md, .cursor/skills/yieldomega-guardrails/SKILL.md.

Verification checklist for @brouie

  • On /timecurve, connect wallet, open Advanced, choose Pay with ETH (or USDM), move the CL8Y slider — primary button shows Refreshing quote… and is disabled until the quoter settles, then returns to Buy CHARM + enabled.
  • CL8Y pay mode: button stays Buy CHARM (no spurious refresh label) and behavior unchanged.
  • After a quote error (e.g. broken route), CTA remains blocked as before; dismiss/error paths unchanged.
  • Optional: bash scripts/e2e-anvil.sh — frontend unit tests + typecheck were run; full Anvil script hit contract size limit in this agent environment (deploy step), so please confirm E2E green in your CI or local Foundry profile.

Issue left open per request — close when you are satisfied.

Implemented on `main` (05c7ebd): Kumbaya quoter loading now uses **`isPending || isFetching`** so background refetches cannot leave the Simple **Buy** control enabled with a stale quote after slider/input changes. Primary CTA shows **Refreshing quote…** while `swapQuoteLoading` is true (ETH/USDM pay modes). Docs: `docs/frontend/timecurve-views.md` (Buy quote refresh), `docs/testing/invariants-and-business-logic.md` (#timecurve-simple-kumbaya-quote-refresh-issue-56), `docs/testing/e2e-anvil.md`; crosslinks in `skills/README.md`, `skills/play-timecurve-doubloon/SKILL.md`, `.cursor/skills/yieldomega-guardrails/SKILL.md`. **Verification checklist for @brouie** - [ ] On `/timecurve`, connect wallet, open **Advanced**, choose **Pay with ETH** (or USDM), move the CL8Y slider — primary button shows **Refreshing quote…** and is disabled until the quoter settles, then returns to **Buy CHARM** + enabled. - [ ] **CL8Y** pay mode: button stays **Buy CHARM** (no spurious refresh label) and behavior unchanged. - [ ] After a quote error (e.g. broken route), CTA remains blocked as before; dismiss/error paths unchanged. - [ ] Optional: `bash scripts/e2e-anvil.sh` — frontend unit tests + typecheck were run; full Anvil script hit **contract size limit** in this agent environment (deploy step), so please confirm E2E green in your CI or local Foundry profile. Issue left **open** per request — close when you are satisfied.
Brouie commented 2026-04-26 04:10:37 +00:00 (Migrated from gitlab.com)

@PlasticDigits closing this on source review of 05c7ebd.

AC1 (label + disabled during re-quote)

  • TimeCurveSimplePage.tsx L564-569: label flips to 'Refreshing quote…' when payWith !== 'cl8y' && session.swapQuoteLoading
  • L247-252: nonCl8yBlocked includes swapQuoteLoading under same payWith !== 'cl8y' guard
  • L253-260: buyDisabled includes nonCl8yBlocked, wired to <motion.button disabled={buyDisabled}> at L547
  • Label change + disabled state fire together on the same trigger. CL8Y pay mode unaffected (no Kumbaya quote to wait for), which is correct.

AC2 (returns to normal on fresh quote)

  • useTimeCurveSaleSession.ts L817: swapQuoteLoading: quoteEnabled && (quotePending || quoteFetching)
  • Fix: previously quotePending only, which left isFetching-without-isPending background refetches invisible. Now both register as loading and clear together when the fresh quote settles.

AC3 (#52 race disappears)

  • anvil-wallet-writes.spec.ts L77-82: test now expects toBeEnabled({ timeout: 120_000 }) on /^Buy CHARM$/i before click. Anchored regex avoids matching 'Refreshing quote…'. Race goes away naturally - test cant click during the re-quote window.

Invariant documented in docs/testing/invariants-and-business-logic.md and docs/frontend/timecurve-views.md.

Closing.

@PlasticDigits closing this on source review of 05c7ebd. AC1 (label + disabled during re-quote) - TimeCurveSimplePage.tsx L564-569: label flips to 'Refreshing quote…' when payWith !== 'cl8y' && session.swapQuoteLoading - L247-252: nonCl8yBlocked includes swapQuoteLoading under same payWith !== 'cl8y' guard - L253-260: buyDisabled includes nonCl8yBlocked, wired to <motion.button disabled={buyDisabled}> at L547 - Label change + disabled state fire together on the same trigger. CL8Y pay mode unaffected (no Kumbaya quote to wait for), which is correct. AC2 (returns to normal on fresh quote) - useTimeCurveSaleSession.ts L817: swapQuoteLoading: quoteEnabled && (quotePending || quoteFetching) - Fix: previously quotePending only, which left isFetching-without-isPending background refetches invisible. Now both register as loading and clear together when the fresh quote settles. AC3 (#52 race disappears) - anvil-wallet-writes.spec.ts L77-82: test now expects toBeEnabled({ timeout: 120_000 }) on /^Buy CHARM$/i before click. Anchored regex avoids matching 'Refreshing quote…'. Race goes away naturally - test cant click during the re-quote window. Invariant documented in docs/testing/invariants-and-business-logic.md and docs/frontend/timecurve-views.md. Closing.
Brouie (Migrated from gitlab.com) closed this issue 2026-04-26 04:10:55 +00:00
PlasticDigits commented 2026-04-26 07:51:35 +00:00 (Migrated from gitlab.com)

mentioned in issue #66

mentioned in issue #66
Brouie commented 2026-04-27 02:46:34 +00:00 (Migrated from gitlab.com)

mentioned in issue #72

mentioned in issue #72
Brouie commented 2026-04-27 04:01:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #74

mentioned in issue #74
PlasticDigits commented 2026-04-28 06:44:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #87

mentioned in issue #87
PlasticDigits commented 2026-05-18 13:05:22 +00:00 (Migrated from gitlab.com)

mentioned in commit e79492835b

mentioned in commit e79492835b95850bfd313b7e113693b1e60aaf0b
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#56
No description provided.