Skip to content

feat(courses): any three courses free, a course catalog, and paying on the site - #1632

Merged
gHashTag merged 1 commit into
mainfrom
feat/courses-three-free-pay-on-site
Oct 13, 2026
Merged

gHashTag merged 1 commit into
mainfrom
feat/courses-three-free-pay-on-site

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Why

The owner (2026-10-13) reported that readers could not subscribe. Production showed why:

  • The bot rung lost everyone. Zero course keys have been minted since the ladder went live on 2026-10-11. The second course sent readers to the Telegram bot, and that is where they stopped.
  • Locked courses lost their name. On a locked course, the gate replaced the overview, so the course title disappeared.
  • Courses were hard to find. They sat as 9 lines inside the header's "Pages" menu, among 28 other links, and no page listed them.

The owner also asked for:

  • payment on the site without the bot;
  • easier navigation;
  • the best practices for selling a subscription.

The owner approved hand-written TS for this work (owner-approved-foreign).

What

  • Rules VERSION 2, vendored byte for byte from feat(course-access): any three courses free, then the club (rules VERSION 2) t27#9357:
    • Any three courses are free; the club is needed from the fourth on. No rung sends a reader to the bot.
    • courseAccess.logic.ts was rebuilt from the new spec. Compared over 8448 inputs, it differs from the old build only at stepOf(1), which changed from 1 to 0.
    • courseAccess.gen.ts changes only VERSION = 2.
  • #/courses catalog:
    • One card per course, with a badge saying what a press will do: yours, free or club.
    • A counter of free courses left ("Free courses left: N of 3"). It is computed from the rules' own step_of, not a hard-coded 3.
    • One "Continue where you left off" button.
    • "Courses" now has its own place in the header and in the mobile menu.
  • Course page:
    • A locked course keeps its kicker, title and description.
    • Breadcrumbs are now Courses > course > module > lesson.
    • A free gate says which free course this will be, and warns on the last one.
    • The paid gate has a "Not now: back to the courses" link.
  • Paying on the site (render API in gHashTag/999-multibots-telegraf, separate PR):
    • The paid gate reads GET /api/course/checkout. When it returns on with a rouble price, Russian-language readers get an email field and a "Pay N roubles" button.
    • The button posts to POST /api/course/checkout and redirects to Robokassa. Robokassa returns the reader to #/<route>?order=<id>.
    • The page polls /api/course/checkout/order until the render has the payment, then stores the paid key and opens the course.
    • It is one payment for the club's days, with no auto-renewal.
    • Readers in other languages, and every reader while the render has no price set, keep the existing bot button. Shipping this PR first is therefore a no-op for payment.
  • Words: every new word (25 of them) is in specs/ladder/course-ladder.t27, with Russian in i18n/course-ladder.ru.json.

Checks

  • check:course-ladder: up to date, 43 words in 2 languages.
  • test:course-ladder, test:course, check:course and check:course-pages pass.
  • qa/og-preview-contract, qa/docs-spec-contract and check:aria pass.
  • vite build succeeds.
  • tsc -b reports no errors in the touched files.
  • eslint on the touched files reports 2 errors, both already on main: unused BASE in Navigation and setKey in an effect in Lesson.

Browser check on a local dev server:

  • Three courses opened free.
  • The fourth showed the club gate with the course title and breadcrumbs.
  • With a mocked render, the checkout path worked end to end: email, then the post body, then return with ?order=, then "Waiting for the payment...", then "Payment received", with the course open and the key stored.
  • With the language set to English, the rouble form was hidden and the bot button stayed.
{
  "version": 1,
  "head_sha": "15367b74a5a68a916b1d2b756aa2f1904d34735b",
  "summary": "Readers could not subscribe: zero course keys were minted after the ladder sent the second course to the Telegram bot. Now any three courses are free, a catalog at #/courses lists every course with what a press will do, a locked course keeps its title, and Russian readers can pay for the club in roubles on the site once the render enables it.",
  "changes": [
    "specs/ladder/course-access.t27, courseAccess.logic.ts, courseAccess.gen.ts: rules VERSION 2 vendored from gHashTag/t27#9357, any three courses free, then the club",
    "specs/ladder/course-ladder.t27 and i18n/course-ladder.ru.json: 25 new words for the catalog, the free counter and paying on the site; courseLadder.generated.ts regenerated",
    "src/pages/Courses.tsx and main.tsx: the #/courses catalog with badges, free courses left and one continue button",
    "src/components/Navigation.tsx and index.css: Courses gets its own place in the header and the mobile menu",
    "src/pages/Course.tsx and Course.css: breadcrumbs, a locked course keeps its title, free slot warnings, and the on-site rouble payment form shown only to Russian readers when the render offers it",
    "src/lib/courseLadder.ts: freeLeft, checkoutOffer, startCheckout and readOrder against the render's checkout API"
  ],
  "tests": [
    {
      "command": "npm run check:course-ladder && npm run test:course-ladder",
      "status": "passed",
      "result": "43 words in 2 languages up to date; ladder tests pass",
      "evidence": "local run in apps/website at 15367b74a"
    },
    {
      "command": "npm run check:course, test:course and check:course-pages",
      "status": "passed",
      "result": "every course has its menu entry and pages; course tests pass",
      "evidence": "local run in apps/website at 15367b74a"
    },
    {
      "command": "node qa/og-preview-contract.mjs, qa/docs-spec-contract.mjs and npm run check:aria",
      "status": "passed",
      "result": "all three contracts pass with the new route",
      "evidence": "local run in apps/website at 15367b74a"
    },
    {
      "command": "npx vite build",
      "status": "passed",
      "result": "build succeeds in about 15 seconds",
      "evidence": "local run in apps/website at 15367b74a"
    },
    {
      "command": "browser walk on the local dev server with a mocked render checkout",
      "status": "passed",
      "result": "three courses open free, the fourth shows the club gate with title and breadcrumbs, a mocked order goes from waiting to paid and opens the course; English readers see only the bot button",
      "evidence": "local run on port 5199 at 15367b74a"
    }
  ],
  "limitations": [
    "Paying on the site stays off until the render serves /api/course/checkout with a price; until then the gate keeps the Telegram bot button.",
    "The rouble price is derived on the render from the club's 10,000 Stars at the CBR rate; that render change is a separate gHashTag/999-multibots-telegraf PR.",
    "Paying on the site with crypto is not built yet; it needs a web buyer identity on the render first.",
    "Two eslint errors in touched files predate this change: an unused BASE in Navigation and a setState inside an effect."
  ],
  "tags": [
    "Courses",
    "Payments",
    "UX"
  ],
  "blog": {
    "title": "Three courses free, and the club paid on the site",
    "summary": "The ladder lost every reader at the Telegram bot rung; this PR makes three courses free, adds a catalog, and prepares paying on the site.",
    "outline": [
      "Production showed zero course keys minted since the ladder went live: the second course sent readers to a Telegram bot, and nobody came back.",
      "The rules moved to VERSION 2 in gHashTag/t27: any three courses are free, the club opens the rest, and a bot key a reader already holds keeps working.",
      "A catalog at #/courses now says on every card what a press will do, counts the free courses left, and offers one lesson to continue.",
      "Russian readers will pay for the club in roubles on the site, priced from the club's 10,000 Stars at the CBR rate, once the render turns checkout on."
    ]
  }
}

🤖 Generated with Claude Code

…n the site

Owner, 2026-10-13: readers could not subscribe -- zero course keys were minted
since the ladder went live, because the second course sent them to the
Telegram bot and they stopped there.

- Rules VERSION 2 vendored from gHashTag/t27 (specs/ladder/course-access.t27,
  logic and gen-ts regenerated): any three courses free, the club from the
  fourth on, no rung sends a reader to the bot.
- #/courses: every course on one page, each card saying what a press will do
  (yours / free / club), the count of free courses left, and the one lesson to
  continue. "Courses" stands in the header instead of nine lines inside
  "Pages".
- A locked course keeps its name, description and breadcrumbs
  (Courses > course > module > lesson); the gate used to replace them.
- The paid gate takes the club in roubles on the site through the render's
  /api/course/checkout (Robokassa, one payment, no auto-renewal), comes back
  with ?order= and opens the course once the render has the payment. Russian
  readers only; without a price on the render the gate keeps the bot button.
- Every new word is in specs/ladder/course-ladder.t27 and its Russian bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag gHashTag added the owner-approved-foreign Owner approved hand-written non-t27 code in this PR label Oct 13, 2026
@github-actions github-actions Bot added the status:in-progress 🔵 Agent working label Oct 13, 2026
@tri-claw

tri-claw Bot commented Oct 13, 2026

Copy link
Copy Markdown

Summary by t27-bees

Checked by the t27 compiler

t27c parse, parse-complete and typecheck on each changed .t27 file at 15367b7. Nothing was generated, built or run. A pass means the file is well formed, not that it is right.

  • apps/website/specs/ladder/course-access.t27: parses and typechecks.
  • apps/website/specs/ladder/course-ladder.t27: parses and typechecks.

Read by the model

Worth a look

  • apps/website/src/pages/Course.tsx (lines ~420–470): The payment polling effect captures valid and keys from the initial render. When the order is confirmed, it uses valid.bot (stale) and spreads readKeys() (fresh) but then calls setValid({ bot: valid.bot, paid: true }) with the stale valid.bot. If the bot key became valid after mount, the paid state would lose it. The same effect also calls setKeys(held) and setOpened(...) with the stale valid in openCourse(C.id, o, now).

  • apps/website/src/pages/Course.tsx (line ~435): The polling loop runs for up to 10 minutes (600,000 ms) with a 2.5 s interval. If the user navigates away, live becomes false but the current await still resolves, then the loop exits. However, navigate(pathname, { replace: true }) is called after setPaying('done') — if the component unmounted, this navigation may error or do nothing, leaving the user on a page that thinks payment is done but hasn't stored the key.

  • apps/website/src/lib/courseLadder.ts (lines 68–75): freeLeft loops up to 255 times calling stepOf on each iteration. Since the spec defines only three free steps, this is equivalent to Math.max(0, 3 - opened.length) and the loop is unnecessary work on every render of the catalog and course pages.

  • apps/website/src/pages/Courses.tsx (lines 70–85): valid starts as { bot: false, paid: false } and is updated asynchronously in an effect. On the first render, courseState receives this initial valid, so every card may show the wrong badge (e.g., "Club" instead of "Free") until the effect completes. The catalog has no loading state for this.

  • apps/website/src/pages/Course.tsx (line ~490): ladder.pay closes over C.route from the useLadder parameter, which is stable. However, ladder is returned from useLadder and passed to PayForm and LadderGate. If useLadder re-runs (it has no dependency array, so it runs once), the returned object is new each render, causing child re-renders. Not a bug, but unnecessary churn.


Read: 13 files, +610 -38, the whole diff. Head 15367b7. Model: openai-compatible/nvidia/nemotron-3-ultra-550b-a55b. Commands: @t27-bees review | summary | help | pause | resume. Free for every repository.

@gHashTag
gHashTag merged commit 3ccc99d into main Oct 13, 2026
21 of 22 checks passed
@github-actions github-actions Bot added status:completed Done and removed status:in-progress 🔵 Agent working labels Oct 13, 2026
github-actions Bot added a commit that referenced this pull request Oct 13, 2026
Merge pull request #1632 from gHashTag/feat/courses-three-free-pay-on-site

feat(courses): any three courses free, a course catalog, and paying on the site
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

owner-approved-foreign Owner approved hand-written non-t27 code in this PR status:completed Done

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant