@meterbox/react
v0.1.14
Published
Embeddable React components for Meterbox: pricing tables, subscription preview, credit balance and usage charts. Presentational by default, with data fetched by your backend via @meterbox/node.
Maintainers
Readme
@meterbox/react
Drop-in React components for Meterbox: pricing tables, plan comparison,
subscription preview, credit balance + grants + utilization, and usage
charts. Depends on @meterbox/node
for the typed API client.
Each component supports two modes:
- Presentational: your backend fetches via
@meterbox/nodeand passes the body in as props. Default behavior. No API key in the browser. - Self-fetching:
<SubscriptionPreview>,<PlanChangeCard>,<CreditBalance>,<CreditGrantsTable>, and<CreditUtilization>can fetch their own data from the/v1/customer-portal/*endpoints using a short-lived customer-scoped JWT minted server-side (POST /v1/tokens/:id). PasscustomerId+customerTokeninstead of the data prop.
For a complete hosted-feel portal in one component, see
@meterbox/customer-portal.
Install
npm install @meterbox/react react react-domreact and react-dom are peer deps (≥17).
Components
import {
PricingTable,
PlanComparison,
SubscriptionPreview,
PlanChangeCard,
CreditBalance,
CreditGrantsTable,
CreditUtilization,
InvoiceHistory,
UsageChart,
CheckoutButton,
} from "@meterbox/react";<PricingTable plans currentPlanId? currency? onSelect? customerEmail? branding? selectedPlanId? onPlanClick? renderFooter? /> or <PricingTable publishableKey … />
On a public page with no backend of its own, pass a
publishable key (mb_pub_..., from Settings > API keys) instead of plans.
The table then reads the public pricing snapshot,
GET /v1/catalog/pricing-page: published plans with display fields only. A
publishable key is safe in page source and unlocks nothing else.
Renders the plan catalog as a comparison grid. Highlights currentPlanId
if supplied, picks a localized price when currency matches a row in
plan.localized_prices. A monthly/annual toggle renders automatically
when at least one plan has annual_free_months set. Selecting "Annual"
shows the one-time prepaid total, not that total divided back into a
misleading "$X/mo" figure. onSelect(plan, billingInterval) receives
whichever interval was selected, ready to hand straight to
<CheckoutButton billingInterval>. PricingTable.annualPriceUsd(plan) is
exposed as a static if you need the same total outside the grid.
PricingTable.fetchPricingPage({ publishableKey, baseUrl? }) resolves to the public pricing
snapshot as { plans, branding }, each plan in the shape <PricingTable plans> takes: for a page
that uses the same plans elsewhere (a sign-up form's plan picker), or shows its own until the live
ones arrive.
From the snapshot, the featured plan is highlighted with its badge ("Most
popular"), a plan's free trial shows as "14-day free trial", and, without
onSelect, each plan's button is its own call-to-action link (e.g. Enterprise
to a contact page); customerEmail fills {CUSTOMER_EMAIL} in it.
Plans from the snapshot (or the customer portal's plan list) also carry
usage. With showUsage (off by default), each card then shows what an AI
product's customers actually pay for usage: the amounts included each month,
what happens past them (billed, stopped until next month, or an upgrade), and
every model's input and output rates per 1M tokens, tier ladders included.
A plan lists its included credits first ("1,500 Bits a month", in the tenant's credit unit), then
its bullet_details, which GET /v1/plans, the pricing snapshot and the customer portal's plans
all carry: each feature by its name on the plan, else by its label with its description behind an
info icon, by its description when it has no label, and by its id only when it has neither. Without them, it
lists its bullets, else its features ([{ id, name }]) by name, else by id. A
pricing-page plan with a public promotion shows its promotional price, the regular one struck
through, and the code to use ("Save 50% with code LAUNCH50 for 3 months").
A plan whose stripe_price_monthly_usd is null has no fixed price and shows "Custom". A plan's
own cta.label names its button, and its cta.url may also be a path on your site
(/signup?plan=pro). A plan's badges (an array of text) show next to its badge.
For a catalog you manage rather than sell from, such as an admin page: selectedPlanId and
onPlanClick(plan) make each card selectable, and renderFooter(plan) puts your own content
(counts, edit buttons) where each card's button would be. A click inside it doesn't select the
card.
Base URL: <MeterboxProvider baseUrl>
The components call https://meterbox.ai, so there's nothing to set. To
point them elsewhere, wrap them once in <MeterboxProvider baseUrl="…">; a
component's own baseUrl prop wins over both. useBaseUrl() reads the
resolved URL for your own calls.
Branding: <BrandingProvider branding>
Every component styles itself from --mbx-* CSS variables (--mbx-accent,
--mbx-text, --mbx-card, --mbx-border, --mbx-font, --mbx-radius and the
button variables), with Meterbox's defaults as fallbacks. Wrap them in
<BrandingProvider branding={branding}> to apply your branding from
Settings → Branding (GET /v1/settings/branding): logo, colors, font and
button style. <PricingTable publishableKey> applies it from the
pricing snapshot by itself, and any component's surroundings can set the
variables in CSS. useBranding() reads the values (e.g. the logo), and
theme exports the variable references for styling your own elements to
match.
Language: <LocaleProvider locale messages?>
Wrap the components in <LocaleProvider locale="fr"> to show them in your
customer's language (the ones the customer portal uses: the pricing table,
plan change and preview, credits, invoices and billing details): English (the default), Spanish (es), French (fr) and
German (de) are built in, picked by the locale's language (fr-CA uses
French). Dates and amounts are formatted for the locale. messages overrides
any string by its key ({ "cancel.button": "End my plan" }), or translates
them all for another language; anything it leaves out stays in English. The
keys and English text are in src/i18n.js. Plan names,
features and offers are shown as you entered them. useI18n() gives your own
elements the same t(key, vars), date(iso) and money(amount, currency).
<BillingDetailsButton customerId customerToken returnUrl? label? onSession? onError? />
Sends the customer to Stripe's own Billing Portal to update their payment
method and billing details (address, tax IDs, email, name, phone) and see
their invoices, so payment details never pass through your app or Meterbox.
POSTs to /v1/customer-portal/:id/billing-details and redirects; Stripe
sends them back to returnUrl (default: the current page). Cancelling and
plan changes aren't offered there: they stay in Meterbox's own flows. It
works once the customer has a Stripe account and the tenant has enabled
checkout_via_meterbox, which GET /v1/customer-portal/:id/me reports as
billing_details_available.
<PlanComparison from to onSwitch? />
Side-by-side comparison of two plan records with added / removed feature
deltas. Pair with <SubscriptionPreview> for the prorated math.
<CheckoutButton planId successUrl cancelUrl customerId customerToken billingInterval? label? onSession? onError? />
One-tap Stripe Checkout for a plan change. POSTs to
/v1/customer-portal/:id/checkout (customer JWT, no API key in the
browser) and redirects to the returned Stripe URL. billingInterval
("monthly", the default, or "annual") only takes effect for a plan with
annual_free_months configured; requesting "annual" on a plan that
doesn't offer it surfaces a 400 via onError rather than redirecting.
<SubscriptionPreview preview? | customerId customerToken planId planVersion? />
Renders the response of POST /v1/customers/:id/subscription/preview (admin)
or POST /v1/customer-portal/:id/subscription/preview (customer JWT):
prorated charge, MRR delta, effective date.
<PlanChangeCard preview? scheduledChange? allowSchedule? | customerId customerToken planId planVersion? fetch? onConfirm? onCancelScheduled? onChanged? />
Composes <SubscriptionPreview> with the actions it doesn't have: actually
applying or scheduling the change, and cancelling a pending scheduled one.
Two modes:
- Presentational: pass
preview(already fetched) plusonConfirm(effective)/onCancelScheduled()callbacks that return promises; you own how the request is actually made (e.g. proxied through your own backend, session-authenticated, holding no customer JWT). - Self-fetching:
customerId+customerToken+planId, noonConfirm/onCancelScheduledsupplied: fetches its own preview AND posts confirm/cancel directly toPOST /v1/customer-portal/:id/subscription/changeandDELETE /v1/customer-portal/:id/subscription/scheduled.
scheduledChange (e.g. from GET /v1/customer-portal/:id/me's
scheduled_change field) renders a banner with its own Cancel action ahead
of the preview. allowSchedule (default true) hides "Schedule for end of
period". Set false if nothing on your side reconciles a change applied
later, on its own schedule, back into wherever you cache plan state.
onChanged(result) fires after either action succeeds, for refreshing your
own cached plan/schedule state. It does not control whether the card stays
mounted; keep whatever render condition shows the card independent of the
plan id it just changed to, or the card (and the confirmation message it's
showing) unmounts the instant your own state catches up.
<CreditBalance balance? | customerId customerToken; threshold? onTopUp? />
Compact balance widget. If balance_usd < threshold, shows a top-up CTA
wired to onTopUp. Self-fetches GET /v1/customer-portal/:id/credits in
self-fetch mode.
<CreditGrantsTable grants? balance_usd? | customerId customerToken; filter? />
Table of active grants: source, amount, remaining, expiry, status. Pass
filter={(g) => ...} to slice the rows.
<CreditUtilization series? | customerId customerToken from? to? granularity?; height? />
Inline SVG line + area chart of credit balance over time, fed by
GET /v1/customer-portal/:id/credits/timeseries.
<InvoiceHistory invoices? | customerId customerToken />
The invoices the customer received, newest first: date, number, status and
amount, with a link to download each Stripe invoice's PDF and to its hosted
page (labelled "Pay" while it's due). Self-fetches
GET /v1/customer-portal/:id/invoices with the customer token.
<UsageChart series valueKey? height? label? />
Tiny inline SVG bar chart of a time series, typically the day-grain rollup
from GET /v1/ledger/:id?grain=day. valueKey picks the dimension
(amount_usd default, api_calls, input_tokens, output_tokens).
End-to-end shapes
Presentational (server fetches, client renders):
// app/server.tsx (Next.js Server Component, or any backend handler)
import { getV1Plans, MeterboxClient } from "@meterbox/node";
new MeterboxClient({ apiKey: process.env.METERBOX_API_KEY });
export default async function PricingPage() {
const { data } = await getV1Plans();
return <PricingTableClient plans={data.plans} />;
}// app/PricingTableClient.tsx
"use client";
import { PricingTable } from "@meterbox/react";
export function PricingTableClient({ plans }) {
function handleSelect(plan, billingInterval) {
// route to checkout, e.g. via <CheckoutButton planId={plan.id} billingInterval={billingInterval} ...>
}
return <PricingTable plans={plans} currency="USD" onSelect={handleSelect} />;
}Self-fetching (browser-safe, customer JWT):
'use client';
import { SubscriptionPreview, CreditBalance } from '@meterbox/react';
// The customer JWT is minted server-side via POST /v1/tokens/:id
// and handed to the client (e.g. as a cookie / SSR prop). It is scoped to
// one customer (claim `sub === customerId`) and short-lived.
export function PortalBits({ customerJwt }) {
return (
<>
<SubscriptionPreview
customerId="cus_42"
customerToken={customerJwt}
planId="pro"
/>
<CreditBalance
customerId="cus_42"
customerToken={customerJwt}
threshold={10}
onTopUp={(b) => /* mint a top-up session */}
/>
</>
);
}