billing-kit-components

Synced from billing-kit-components/README.md — the repo is canonical.

The UI layer for billing-kit. billing-kit is a headless, Apache-2.0 billing library that states plainly it "is not a tax engine, a dunning system, a pricing UI, an accounting system, or a payment processor. It has interfaces where those attach and no opinions inside them." This repository is those opinions — the React components that attach to billing-kit's public types: usage meters, pricing tables, ledger and invoice views, a checkout panel, a superadmin console, and the headless hooks under them.

flowchart LR
    subgraph reg["billing-kit-components — proprietary, per seat"]
        items["19 components + 3 hooks<br/>source, shadcn schema"]
    end

    gate{{"seat gate<br/>ai_member_cloud entitlement"}}

    subgraph app["your app"]
        direction TB
        src["component source<br/>yours to own and restyle"]
        bk["billing-kit — Apache-2.0<br/>Money · Quantity · types"]
        src -->|reads types| bk
    end

    cli(["npx shadcn add"]) -->|seat token| gate
    gate -->|valid| items
    items -->|writes source| src

    classDef prop fill:#7c3aed,stroke:#6d28d9,color:#ffffff;
    classDef apache fill:#0d9488,stroke:#0f766e,color:#ffffff;
    classDef gate fill:#8a6212,stroke:#6d4d0e,color:#ffffff;
    class items prop;
    class bk apache;
    class gate gate;

The gate (amber) checks the seat once, at install. After that the source is yours (purple, now in your repo) and reads billing-kit's Apache-2.0 types (teal) like any hand-written component. Nothing phones home.


This is NOT open source

billing-kit-components is commercial, proprietary software, licensed per developer seat, yearly. It is not Apache-2.0, not AGPL, not MIT, not free software. See LICENSE for the terms. If you do not hold a current, paid seat, you have no licence to use the component source this registry distributes.

The dependency direction is worth stating: billing-kit is Apache-2.0 and this commercial library depends on it. Nothing here restricts billing-kit. This licence governs only the component source served from this registry.

The enforcement model is Tailwind Plus's, not a runtime lock:

  • The licence is a legal instrument, enforced by contract plus an authentication gate in front of the registry.
  • The gate runs at add time only — when the shadcn CLI fetches a component — and binds to ai_member_cloud entitlements for the seat check. See docs/LICENSING.md.
  • Nothing phones home from installed source. No component this registry ships contains a licence check or a network call home. Once installed, the source is yours, and it behaves like any hand-written shadcn component. A runtime lock punishes the paying customer and stops nobody; we do not ship one.

This package is private and is never published to npm — distribution is by copy-paste registry, below, not by npm install.


How it is distributed

A shadcn-compatible copy-paste registry. shadcn add writes component source into your repository; you own and restyle it from there. The registry is built to the official shadcn registry schema, so the standard CLI installs it unmodified.

# 1. Put your per-seat token (from your ai_member_cloud seat) in .env.local:
#    BILLING_KIT_COMPONENTS_TOKEN=aim_live_…
# 2. Add the namespace to your app's components.json (see docs/REGISTRY.md).
# 3. Install a component — the CLI attaches your token and fetches the source:
npx shadcn@latest add @billing-kit/pricing-table

The full registry contract — the index, the item schema, the four component groups, and the seat gate — is in docs/REGISTRY.md.


What is in the registry

Nineteen components across four groups, plus the three hooks above.

Group Components
Usage & metering usage-meter · quota-bar · rate-display · usage-chart
Pricing & plans pricing-table · plan-card · plan-picker · tier-comparison · promo-code · plan-change-control
Ledger, invoices & balance balance-badge · ledger-table · invoice-view
Checkout & superadmin checkout-panel · stripe-payment-fields · paddle-checkout · price-editor · promo-manager · entitlement-toggles

The checkout components use the provider's hosted fields (Stripe Elements, Paddle) — raw card data never touches the component or your server. The superadmin set treats prices as versioned, never mutated in place, and promo codes carry redemption caps and expiry as first-class fields.


The demo

demo/ is a single page that renders every component against real billing-kit fixtures. It is a genuine shadcn consumer of this registry — its @/… aliases resolve exactly as a customer's project would — so the demo running is proof the components run, not a mock of them.

cd demo
pnpm install
pnpm dev          # → http://localhost:5500

What the demo is checked to do, on every change:

Check What it proves
tsc --noEmit the components satisfy their own prop contracts as a consumer sees them
vite build all 19 components bundle — 76 modules
vitest run (jsdom) the whole page mounts with real fixtures without throwing, and the minor-unit integer 9900 never reaches the DOM

The Stripe/Paddle checkout components need live provider keys and hosted fields, so they are excluded from this static page rather than rendered against a fake key. The superadmin set is pure UI and is in the page.


The headless hooks

Alongside the components, in the Vercel-AI-SDK style, are headless hooks typed against billing-kit's real shapes. Each is a copy-paste registry item and each ships a plain, framework-agnostic loader beside its React binding, so the loader is usable from a server component or a non-React runtime.

Hook Reads Returns
useUsage an app route over billing-kit queryUsage StoredUsageEvent[], quantities rebuilt as Quantity — never Number()
useBalance an app route over billing-kit balance a Money, rebuilt from MoneyJSON — never divided by 100
useEntitlement the app's own entitlement store a subject's grants, with isEntitled / limitOf (this is a product feature, not the seat gate)

The load-bearing rule these hooks exist to hold: every amount is a billing-kit Money and every quantity a Quantity, reconstructed from its exact wire form and formatted through the money lib — never a JavaScript number.

flowchart LR
    db[("billing-kit<br/>queryUsage · balance<br/>SQL — server only")]
    route["your HTTP route<br/>MoneyJSON wire form"]
    hook["useBalance · useUsage<br/>rebuilds Money and Quantity"]
    comp["component<br/>formatMoney()"]
    ui(["$99.00 &nbsp; ¥1,200 &nbsp; -$24.50"])

    db --> route --> hook --> comp --> ui

    bad["minor / 100<br/>wrong for JPY and KWD; a double drifts"]
    comp -. never .- bad

    classDef ok fill:#0d9488,stroke:#0f766e,color:#ffffff;
    classDef stop fill:#9e2b2b,stroke:#7f2222,color:#ffffff;
    class hook,comp ok;
    class bad stop;

A hook that did Number(minor) / 100 would defeat the type system billing-kit is built on: / 100 is wrong for a third of ISO 4217 (JPY has no minor unit, KWD has three), and a double cannot tell an exact 19.99 from one that already drifted. The demo's render test asserts this directly — the minor-unit integer never reaches the DOM.

Why a fetch and not a direct call: billing-kit's queryUsage and balance take a SqlExecutor and run SQL — they are server code and must not enter a browser bundle. The hooks read from HTTP routes the app exposes over them, and rebuild the exact types on arrival.


Repository layout (what this package owns)

billing-kit-components/
  package.json            private; billing-kit + react are peer dependencies
  tsconfig.json           type-check only (noEmit); bundler resolution to billing-kit's source
  tailwind.config.ts      workspace config; colours live in registry/lib/tokens.css
  .env.example            customer seat token and (separately) registry-operator secrets
  registry.json           the registry index (authored by the registry owner)
  scripts/
    build-registry.mjs    assembles public/r/<name>.json from registry.json + source
  registry/
    hooks/                useUsage, useBalance, useEntitlement (+ a test each)
    lib/                  the money formatting boundary, cn, tokens, tailwind preset
    seat/                 the add-time registry auth gate (never shipped to apps)
    usage/ pricing/ ledger/ admin/      the four component groups (admin = checkout + superadmin)
  demo/                   a single page rendering every component; a real registry consumer
  public/r/               BUILD OUTPUT — one JSON per item, served to the CLI (gitignored)

Development

npm install            # installs dev tooling and links ../billing-kit
npm run typecheck      # tsc --noEmit over the authored source
npm run test           # node --test over the hooks and lib
npm run build:registry # emit public/r/<name>.json from registry.json

build:registry emits one public/r/<name>.json per item from registry.json and the source. It skips (and warns about) any item whose source files are missing rather than emitting a half-built item; pass --strict in a release build to make a missing file a hard error.