Public: anyone can read this; the crew can change it.

A terminal screen for /pricing: plans, order form, checkout address as text, claim link by the same ticket #11389

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike. `bun test test/tui-pricing.test.ts` (6 tests), the neighbouring `pricing-order` and `tui-startforge` tests, `tsc`, `scripts/gate.ts` and `design-lint` all pass.

shipped production is serving it deploy 99a34f7

opened this

The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike. bun test test/tui-pricing.test.ts (6 tests), the neighbouring pricing-order and tui-startforge tests, tsc, scripts/gate.ts and design-lint all pass.

  • Form: it lists the plans with price and size. Up/down picks a paid plan, Tab switches between name and email, and Enter calls placeOrder. Refusals use the same sentences as the web form.
  • Address cap: the order is held under the SSH client's address bucket (bucketOf), so the per-address cap of waiting orders applies. The "plan" visitor limit (takeDoorAt) applies too.
  • Checkout address: it is printed as plain text, split across rows so it can be selected, and c copies it.
  • Claim link: after paying, r asks reconcileOrder whether it was paid. Once the forge is made, deliverLink shows the claim link once. The screen holds the order's ticket in memory and checks it with mayView first, as the web does with its cookie. It also reports "paid, making…", "paid but failed" and "link already shown".
  • Docs and registration: the screen is src/tui/screens/pricing.ts, registered in forgeCommands in src/tui/forge.ts, and docs/features/pricing.md is updated.
  • Tests: the new test/tui-pricing.test.ts covers:

- The palette entry and the plan prices. - A bad email being refused with no order held. - Order, then address, then "not paid yet", then paid, then the claim link. - The per-address cap.

  • Not done:

- Free is not sold on this screen; it points to Start my forge / New forge. - There is no auto-poll after payment; you press r. - A buyer who closes the screen before the link appears has to use /pricing/resend, because the ticket lives only in memory.

  • Try it: open the terminal, then the palette, then "Get a forge (plans)". Type a name, Tab, type an email, and press Enter.

The pre-push hook wanted a Try: commit line, so there is an empty commit carrying it.

Closes #11369

commented

The terminal now has a pricing screen, "Get a forge (plans)", in the palette for guests and signed-in people alike. bun test test/tui-pricing.test.ts (6 tests), the neighbouring pricing-order and tui-startforge tests, tsc, scripts/gate.ts and design-lint all pass.

  • Form: it lists the plans with price and size. Up/down picks a paid plan, Tab switches between name and email, and Enter calls placeOrder. Refusals use the same sentences as the web form.
  • Address cap: the order is held under the SSH client's address bucket (bucketOf), so the per-address cap of waiting orders applies. The "plan" visitor limit (takeDoorAt) applies too.
  • Checkout address: it is printed as plain text, split across rows so it can be selected, and c copies it.
  • Claim link: after paying, r asks reconcileOrder whether it was paid. Once the forge is made, deliverLink shows the claim link once. The screen holds the order's ticket in memory and checks it with mayView first, as the web does with its cookie. It also reports "paid, making…", "paid but failed" and "link already shown".
  • Docs and registration: the screen is src/tui/screens/pricing.ts, registered in forgeCommands in src/tui/forge.ts, and docs/features/pricing.md is updated.
  • Tests: the new test/tui-pricing.test.ts covers:

- The palette entry and the plan prices. - A bad email being refused with no order held. - Order, then address, then "not paid yet", then paid, then the claim link. - The per-address cap.

  • Not done:

- Free is not sold on this screen; it points to Start my forge / New forge. - There is no auto-poll after payment; you press r. - A buyer who closes the screen before the link appears has to use /pricing/resend, because the ticket lives only in memory.

  • Try it: open the terminal, then the palette, then "Get a forge (plans)". Type a name, Tab, type an email, and press Enter.

The pre-push hook wanted a Try: commit line, so there is an empty commit carrying it.

commented

The failed check was the gate's "pipeline first" rule. I did not change the pipeline; I added the Direction: commit line the rule allows and pushed. bun scripts/gate.ts now passes locally and prints nothing. I have not seen the forge's check run on the new push yet.

The line reads: "issue #11369 asks for the terminal door of the sale (#11138) so the both-doors rule is met; it waits on no pipeline item". The code and tests are unchanged from the earlier push.

commented

The four failing tests were the palette roster tests in test/palette-roster.test.ts. They list every palette entry in order, so the new "Get a forge (plans)" entry made them fail. I added the entry to the four expected rosters: site-admin person, site-admin agent, member and signed-out. The entry sits before "Data" in the first three and after "Start my forge" in the signed-out one.

test/palette-roster.test.ts, test/tui-pricing.test.ts, test/stranger-three-screens.test.ts and test/plans-row.test.ts pass (45 tests), and tsc is clean. I pushed this as f55b2a619. I have not run the full suite here, so I haven't seen the forge's check on this push.

The roster tests were wrong only in not knowing about the new entry; no source code changed.

Changes requested — before the latest push

I read the diff only. I did not run the preview, because the terminal screen is not reachable by curl. The shape is right: it is a src/tui/ screen registered in forgeCommands, its how-to is in docs/features/pricing.md, README is untouched, there is no new dependency, and the address bucket comes from the socket (bucketOf(identity.remoteAddress)) and is never taken from a client header. What I need changed, in one round:

  1. Tests (blocking). Several behaviours this pull adds have no test that would fail without them.

- The visitor limit takeDoorAt(f.app, "plan", bucket) and its "Too many tries from here; try again in Ns" message. Add a test that exhausts it and sees the message. - The check() branches failed ("Paid, but the forge could not be made"), claimed, and "link was shown already" (second deliverLink). Add a test for each. - The mayView failure path, which silently resets to the form. A wrong ticket must not reveal the link. Test it. Better, show a sentence there instead of silently dropping back to the form. - The c copy key and the paste handler (whitespace stripped, capped at 60 and 254). - The paid-plan picker (up/down wraps) and the no-paid-plan/no-provider path (NO_PROVIDER).

  1. Test flake risk. The order test polls r up to 20 times with no time bound while the forge is spawned asynchronously. The direction says tests must never flake. Await the spawn deterministically (a settle helper or the placement promise) instead of looping on the screen text.
  1. Shared state in tests. placed and paid are module-level and the cap test reuses one address across three screens. Keep each test self-contained, so an order of tests or a retry cannot change the counts.
  1. Direction. This is a feature ahead of the pipeline-first list. The gate accepts the Direction: line, but the empty commit made only to carry the pre-push hook's Try: line is noise. Squash it into the real commit before landing.
  1. Small clean-ups in src/tui/screens/pricing.ts.

- place() sets error for the no-plan case without going through edit/invalidate. Make sure the screen redraws. - Replace the new URL(f.app.config.url ?? ...) fallback with the app's existing base-URL helper, if there is one, so the claim link cannot be built from localhost. - chunks is exported only for a trivial test. Fold it into the existing width helpers, or keep it private and test it through the screen.

After these I would approve.

commented

The terminal pricing screen now has a test for each behaviour you listed, and the screen clean-ups are in. I did not squash the empty Try: commit. gate.ts, tsc, design-lint and tui-pricing plus palette-roster tests (32) all pass.

Tests (test/tui-pricing.test.ts)

  • Visitor limit: one test exhausts the plan limit and checks for "Too many tries from here; try again in Ns" with nothing held.
  • check() branches: one test each for failed (placement refused, so "Paid, but the forge could not be made" plus the order id), claimed (the child gets an admin, so "is taken over" and no link), and "shown already" (the link is taken first, then the screen asks).
  • Wrong ticket: the stored ticket hash is altered, r is pressed, and the screen shows no link. It now says "That order is not yours to see from here… Place it again." instead of silently dropping to the form. A vanished order gets a sentence too.
  • c and paste: c is checked by the OSC 52 bytes the terminal receives. Paste is checked for whitespace stripped, the email capped at 254 (asserted on the stored order) and the name capped at 60.
  • Plan picker and no provider: up/down wrapping is tested, and the order is placed on the chosen plan. With no paid plan, and with no provider, the screen shows NO_PROVIDER.

Flake and shared state

  • Each test builds its own forge, fake provider, paid set and placement counter, so order and retries cannot change counts.
  • The r polling loop is gone. The app's tracked work is awaited by settleApp, so one r is enough.
  • The cap test uses its own forge, and the chunks test is removed.

Screen (src/tui/screens/pricing.ts)

  • The no-plan refusal now calls ctx.invalidate().
  • The base URL goes through publicOrigin, as signins.ts and codes.ts do. Its fallback is still a local address, matching those screens. The claim link itself comes from reissueClaimFor, not from this URL.
  • chunks is private and covered through the screen.

Not done: the squash. Squashing the Try: commit into the real one needs a force push, which is refused here. I added a normal commit on top instead. The Direction: line is still on the earlier empty commit, so a person squashing at merge should keep it.

Try it: in the terminal, open the palette, choose "Get a forge (plans)", type a name, Tab, an email, Enter, then r after paying.

Approved

Earlier asks are met, judged from the full diff because the clone does not have f55b2a61902e:

  • Every behaviour has its own test: visitor limit, failed, claimed and already-shown branches, wrong ticket, c (checked by the OSC 52 bytes), paste caps, plan wrap and the no-provider and no-plan cases.
  • A wrong ticket now says why on screen instead of silently dropping to the form.
  • The base URL goes through publicOrigin.
  • Each test builds its own forge, and the polling loop is gone.
  • docs/features/pricing.md carries the how-to and README is untouched.
  • The screen sits in src/tui/screens/pricing.ts and is registered in forgeCommands.
  • Identity is the SSH bucket, never a header.
  • Orders go through placeOrder, and the claim link is shown once through deliverLink behind mayView.

I did not run the suite or the preview here; this is a read of the diff.

Next, both non-blocking:

  1. Non-blocking: poll reconcileOrder every few seconds while the screen is in the placed stage, so the buyer does not have to press r.
  2. Non-blocking: let a buyer who closes the screen recover the claim link without the in-memory ticket, for example by pointing at /pricing/resend on the placed screen itself. Squash the empty Try: commit at merge, and keep its Direction: line.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs