Skip to content

Repository files navigation

keeperhub-pricing-ui

Shared pricing cards and comparison table for the KeeperHub app and the marketing landing. One source of truth for both the look and the pricing data.

  • Components ship as a versioned React package (SSR-safe, bundled at build time).
  • Data lives in pricing-data.json and is published to a static URL, so both apps fetch it at runtime and a data change reflects on both with no app redeploy.

What it exports

import {
  PricingCards,
  PricingComparisonTable,
  defaultPricingData,
  defaultPricingCards,
  defaultComparisonColumns,
  defaultComparisonRows,
} from "keeperhub-pricing-ui";
import "keeperhub-pricing-ui/styles.css"; // import once, near your root

Both components are controlled and presentational: they render from data props and delegate every interaction to handlers you pass in. Pass nothing and they render the bundled defaults.

Usage

Zero-config (uses the bundled defaults, good for a quick render or offline fallback):

<PricingCards />
<PricingComparisonTable />

Landing (link CTAs, data fetched at runtime so edits need no redeploy):

// server component: fetch with ISR
const data = await fetch(PRICING_DATA_URL, { next: { revalidate: 300 } })
  .then((r) => r.json())
  .catch(() => defaultPricingData); // fall back to the bundled copy

<PricingCards cards={data.cards} />
<PricingComparisonTable columns={data.comparison.columns} rows={data.comparison.rows} />

App (override the free card with the live PAYG price, wire CTAs to checkout, highlight the current plan):

const cards = data.cards.map((c) =>
  c.id === "free" ? { ...c, fixedPrice: `$${livePaygPrice}` } : c
);

<PricingCards
  cards={cards.map((c) => ({
    ...c,
    cta: { ...c.cta, href: undefined, onClick: (ctx) => startCheckout(ctx) },
  }))}
  onTierChange={(cardId, tierKey) => trackTier(cardId, tierKey)}
/>
<PricingComparisonTable currentColumnKey={currentPlan} />

Data model

pricing-data.json holds cards and comparison. See src/types.ts for the full shapes. Cards support either a fixedPrice (free / enterprise) or tiers (the card renders a selector and derives price + metrics from the selected tier and the billing interval). CTAs are serializable in the JSON (label + href); consumers attach onClick at render time.

How updates propagate

Change What to do Redeploy apps?
Prices, tiers, rows, copy edit pricing-data.json, push to main No. Pages republishes the JSON; both apps pick it up within the ISR window.
Component layout / styling / behavior change src/**, bump version, publish Yes. Bump the dependency in each app and redeploy.

Publishing (manual steps)

The package publishes to GitHub Packages; the data publishes to GitHub Pages. Both run in CI, but the repo and Pages have to be set up once:

  1. Create the KeeperHub/pricing-ui repo and push this folder to main.
  2. In the repo settings, enable Pages with source "GitHub Actions". The deploy-data.yml workflow then serves pricing-data.json at https://keeperhub.github.io/pricing-ui/pricing-data.json.
  3. Add an NPM_TOKEN secret to this repo (an npm automation token for the account or org that owns the package name).
  4. To publish a version: bump version in package.json, tag it (git tag v0.1.0 && git push --tags). The publish-package.yml workflow builds and publishes to the public npm registry.
  5. Consumer repos (app, landing) install it like any public npm package (pnpm add keeperhub-pricing-ui). No auth token, no .npmrc, and it installs unchanged inside Docker builds and CI.

Local development

npm install
npm run build      # tsup -> dist (esm + cjs + d.ts)
npm run typecheck

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages