fix(billing): Checkout org name/subdomain keep what was typed, flag a taken subdomain as you type, and Pay stops latching grey - #616
Conversation
…check the subdomain as they type Three things on the Checkout tab's org bootstrap (the Organization name + Subdomain pair, shown only when a personal account picks Team or Business). 1. The typed values came back as the auto-suggestions. Those two inputs were painted from `ui._defaultOrgName()` / `_defaultOrgIdent()` and nothing ever wrote what was typed anywhere, so the first re-render put the suggestion back. The page re-renders on its own: `visibilitychange` re-syncs the subscription the moment the browser tab regains focus, which is exactly the report — switch tab, come back, the name you entered is gone. Switching billing cycle did it too. Both fields now `watch` their edits into `state.checkout` (input/change/paste/cut, so a mouse paste counts), and orgFieldValue() paints from state, treating "" as a deliberate clear rather than "untouched". 2. A taken subdomain was only reported after pressing Proceed to Checkout. The same payment.validate_org_ident call now runs 450 ms after the typing stops — and once for the auto-suggested value nobody typed, which is derived from the username and just as able to be taken — with the verdict under the field. Deduped and race-guarded on the ident, so a re-render asks nothing and a slow reply cannot label a value the shopper has since changed. A failed round trip stays silent: the check on Pay is still the one that blocks. 3. Proceed to Checkout went grey and dead after that alert. `state: 1` gives ui-core's toggle behavior, whose onAlsoClick flips data-state 1 -> 0 on the very click that submits, and the skin greys `[data-state="0"]` out with pointer-events:none. Invisible while the click ended in a redirect to Stripe; on every path that stays on the page it left a button that could not be pressed again. Dropped the prop — disabled-ness is `dataset.disabled`, which the same skin rule already styles. Verified against the shipped skeleton with a 9-case harness (typed value wins, cleared stays cleared, untouched falls back, section only for team/business, button carries no state, free plan still disabled, verdict slot empty then is-error / is-ok). Non-vacuous: 6 of the 9 fail on the pre-fix file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ty, not :empty Browser-verified on drumee.in and the `:empty` rule never fired: ui-core keeps a `blank` widget inside a Box built with no kids, so the slot element is never childless. The slot therefore stayed in the flow and drew the org section's 12px gap around nothing — 36px between the hint and "Billing Cycle" instead of 24px. The skeleton now stamps `data-empty` and _paintOrgIdentMsg keeps it in step on the incremental path, which is the only one that can change the slot's contents without a re-render. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebd252e365
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| this._orgIdentTimer = setTimeout( | ||
| () => this._checkOrgIdent(ident), | ||
| ORG_IDENT_DEBOUNCE_MS, |
There was a problem hiding this comment.
Invalidate the availability verdict when the input changes
When a user changes one nonempty subdomain to another, this schedules the new check without clearing the existing message or changing _orgIdentChecked. The UI therefore continues to label the new value with the previous value's verdict during the debounce and request, and an in-flight response for the previous value can still pass the guard at line 1738 and repaint that stale result. Clear the displayed verdict and invalidate the outstanding identifier as soon as a different value is scheduled.
Useful? React with 👍 / 👎.
| // | ||
| // Disabled-ness is carried by `dataset.disabled` alone — the same skin | ||
| // rule styles it, and nothing flips it behind our back. | ||
| dataset: isFreePlan ? { disabled: 1 } : undefined, |
There was a problem hiding this comment.
Prevent duplicate checkout requests while Pay is pending
When validation or checkout takes longer than a double-click, the button now remains enabled and every click invokes _proceedToCheckout() independently; there is no pending guard in that handler, so each invocation can issue its own payment.checkout request and create a separate hosted Checkout session (and repeat any promo reservation attempt). Keep the button retriable after an error, but disable or guard it only while the current submission is in flight and restore it on every non-redirect path.
Useful? React with 👍 / 👎.


Promotes the two Checkout-tab fixes verified on
test/ drumee.in. Cherry-picked ontoorigin/preview, so this PR carries only these two commits — and sincepreview..testwasexactly these two, the branch tree is byte-identical to
origin/test(
038251c97185323abaed396b938f9368a7f95743).Reported by Thuận against prod, on the org bootstrap pair (Organization name + Subdomain) that the
Checkout tab shows when a personal account picks Team or Business.
1. The typed values reverted to the auto-suggestions
Both inputs were painted from
_defaultOrgName()/_defaultOrgIdent()and nothing ever storedwhat was typed, so the first re-render put the suggestion back. This page re-renders on its own:
the
visibilitychangere-sync fires the moment the browser tab regains focus — which is exactlythe report (switch tab, come back, the name you entered is gone). Switching billing cycle did it
too.
Both fields now
watchtheir edits intostate.checkout(input/change/paste/cut, so amouse paste counts as well as typing), and
orgFieldValue()paints from state. An empty string istreated as a deliberate clear rather than "untouched", so clearing a field survives a repaint too;
submitting an empty field still falls back to the same auto value.
Same mechanism the tasks board already uses for
board-title-changed.2. A taken subdomain was only reported after pressing Proceed to Checkout
The same
payment.validate_org_identcall now also runs 450 ms after the typing stops, with theverdict inline under the field — and once for the auto-suggested value nobody typed, which is
derived from the username and is just as able to be taken.
Deduped and race-guarded on the ident, so a re-render asks nothing and a slow reply cannot label a
value the shopper has since changed. A failed round trip stays silent: the check on Pay is still
the one that blocks.
validate_org_identis a DB-only read (one proc + one count query) — nothingStripe-shaped.
3. Proceed to Checkout went grey and dead after that alert
state: 1gives the widget ui-core's toggle behavior, whoseonAlsoClickflipsdata-state1 → 0 on the very click that submits — and the skin greys
[data-state="0"]out withpointer-events: none. Invisible while the click ended in a redirect to Stripe, but every paththat stays on the page (the subdomain is taken,
ALREADY_SUBSCRIBED, a network error) left abutton that could not be pressed again without reloading.
The prop is gone; disabled-ness is carried by
dataset.disabled, which the same skin rule alreadystyles and which nothing flips behind our back. The entry fields escaped this only because
entry()gives them aradio, which wins over toggle inbehaviors().The second commit fixes a defect the browser found in the first:
:emptycan never collapse theverdict slot, because ui-core keeps a
blankwidget inside a Box built with no kids. It is keyedoff
data-emptyinstead.Verified
Live on drumee.in, signed in as an account with
domain_id = 1, withpayment.checkoutblocked in the page first (both
window.fetchandXMLHttpRequest.prototype.open) so no Stripesession could be created — 1 attempt blocked, no redirect; the guard was removed and the page
reloaded afterwards.
visibilitychange, and a real browser-tab switchdata-disabled="1", grey,pointer-events: none, click inertAlso: 9-case harness rendering the shipped skeleton (non-vacuous — 6 of 9 fail on the pre-fix
file), ESLint under the CI ruleset clean, all six locale files valid JSON, SCSS compiles.
preview→testneeds a backmerge before the next whole-branch promotion,per the usual squash-merge divergence.
🤖 Generated with Claude Code