Skip to content

fix(billing): Checkout org name/subdomain keep what was typed, flag a taken subdomain as you type, and Pay stops latching grey - #616

Open
EddyOne81 wants to merge 2 commits into
previewfrom
hotfix/billing-org-bootstrap-fields
Open

EddyOne81 wants to merge 2 commits into
previewfrom
hotfix/billing-org-bootstrap-fields

Conversation

@EddyOne81

Copy link
Copy Markdown
Contributor

Promotes the two Checkout-tab fixes verified on test / drumee.in. Cherry-picked onto
origin/preview, so this PR carries only these two commits — and since preview..test was
exactly 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 stored
what was typed, so the first re-render put the suggestion back. This page re-renders on its own:
the visibilitychange re-sync fires 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 as well as typing), and orgFieldValue() paints from state. An empty string is
treated 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_ident call now also runs 450 ms after the typing stops, with the
verdict 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_ident is a DB-only read (one proc + one count query) — nothing
Stripe-shaped.

3. Proceed to Checkout went grey and dead after that alert

state: 1 gives the widget 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, but every path
that stays on the page
(the subdomain is taken, ALREADY_SUBSCRIBED, a network error) left a
button that could not be pressed again without reloading.

The prop is gone; disabled-ness is carried by dataset.disabled, which the same skin rule already
styles and which nothing flips behind our back. The entry fields escaped this only because
entry() gives them a radio, which wins over toggle in behaviors().

The second commit fixes a defect the browser found in the first: :empty can never collapse the
verdict slot, because ui-core keeps a blank widget inside a Box built with no kids. It is keyed
off data-empty instead.

Verified

Live on drumee.in, signed in as an account with domain_id = 1, with payment.checkout
blocked in the page first (both window.fetch and XMLHttpRequest.prototype.open) so no Stripe
session could be created — 1 attempt blocked, no redirect; the guard was removed and the page
reloaded afterwards.

check result
type both fields → visibilitychange, and a real browser-tab switch typed values kept
type a taken subdomain 10 keystrokes → 1 request, red verdict inline
Pay while taken → alert → Close button still live; a second click reaches the handler
Pay with a free subdomain (checkout blocked) button still live, typed values kept
select Free still data-disabled="1", grey, pointer-events: none, click inert
clear the subdomain slot collapses, gap back to 24px

Also: 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.

⚠️ After this merges, previewtest needs a backmerge before the next whole-branch promotion,
per the usual squash-merge divergence.

🤖 Generated with Claude Code

EddyOne81 and others added 2 commits September 21, 2026 00:15
…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>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1713 to +1715
this._orgIdentTimer = setTimeout(
() => this._checkOrgIdent(ident),
ORG_IDENT_DEBOUNCE_MS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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.

1 participant