← All @molecule/* packages · App templates
@molecule/app-billing-reactFeature · billing · App (browser) · v1.0.1 · Apache-2.0
React pricing page + checkout flow that consumes /api/billing/* (provided by @molecule/api-entitlements + the @molecule/api-payments capability, any provider bond)
npm install @molecule/app-billing-react@molecule/app-billing-react is a ready-made billing feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import { PricingPage } from '@molecule/app-billing-react'
import type { PersonalFinanceLimits } from '../tiers'
const Pricing = () => (
<PricingPage<PersonalFinanceLimits>
period="month"
renderLimits={(l) => (
<ul>
<li>{l.maxAccounts} accounts</li>
<li>{l.maxTransactionsPerMonth} transactions / month</li>
</ul>
)}
/>
)Works with: @molecule/app-http, @molecule/app-react, @molecule/app-ui, @molecule/app-ui-react
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
React pricing page + checkout flow for the molecule.dev billing kit.
Components:
<PricingPage /> — public pricing table that fetches /api/billing/tiers
and posts to /api/billing/checkout when the user clicks Upgrade.
<BillingStatusBadge /> — compact account-page status display that
shows the current tier and offers a cancel-subscription button.
<LimitsList> / <LimitsItem> — building blocks for the
renderLimits prop: a stacked checklist row with check / dash icon,
e.g. renderLimits={(l) => ( <LimitsList> <LimitsItem>{l.maxAccounts} accounts</LimitsItem> <LimitsItem included={l.canExport}>Data export</LimitsItem> </LimitsList> )}
Hooks:
usePricingTiers<TLimits>() → UseHttpResult<PricingTiersResponse<TLimits>>
useBillingStatus<TLimits>() → UseHttpResult<BillingStatus<TLimits>>
useStartCheckout() → { data, loading, error, start(priceId) }
useCancelSubscription() → { data, loading, error, cancel() }
The API side of this kit lives in @molecule/api-entitlements +
@molecule/api-payments-stripe. Wire those into your project (any
mlcl flagship template that includes @molecule/api-entitlements
already exposes the /api/billing/* routes), then drop <PricingPage />
onto a /pricing route.
import { PricingPage } from '@molecule/app-billing-react'
import type { PersonalFinanceLimits } from '../tiers'
const Pricing = () => (
<PricingPage<PersonalFinanceLimits>
period="month"
renderLimits={(l) => (
<ul>
<li>{l.maxAccounts} accounts</li>
<li>{l.maxTransactionsPerMonth} transactions / month</li>
</ul>
)}
/>
)
feature
npm install @molecule/app-billing-react @molecule/app-http @molecule/app-react @molecule/app-ui @molecule/app-ui-react react
npm install -D @types/react
BillingActionStateAsync-state shape returned by useStartCheckout / useCancelSubscription.
interface BillingActionState<T> {
/** The most recent response from the action, or `null` before the first call. */
data: T | null
/** True while a request is in flight. */
loading: boolean
/** Last error thrown by the action, or `null` on success. */
error: Error | null
}
BillingStatusSnapshot of the signed-in user's current billing state. Returned by
GET /api/billing/status once the user is authenticated.
interface BillingStatus<TLimits = unknown> {
/** The user's `users.planKey` (`'free'`, `'stripeMonthly'`, etc.). */
planKey: string
/** Tier category — `'free'`, `'pro'`, `'team'`, or any app-defined extension. */
category: string
/** Display name (matches `PricingTierEntry.name` for the active tier). */
name: string
/** Tier-specific limits the user is currently entitled to. */
limits: TLimits
/** True when the user is on the registry default (free) tier. */
isFree: boolean
}
BillingStatusBadgePropsProps for <BillingStatusBadge />.
interface BillingStatusBadgeProps {
/**
* Optional callback invoked after a successful cancel. Most apps will
* navigate the user back to settings or refresh the page from here.
*/
onCanceled?: () => void
/** Optional className applied to the outer wrapper. */
className?: string
/**
* Whether to render the cancel-subscription button when the user is
* on a paid tier. Defaults to `true`.
*/
showCancel?: boolean
}
CancelResponseResponse from POST /api/billing/cancel.
interface CancelResponse {
/** True when the cancellation request was accepted. */
canceled?: boolean
/** Localized error message when cancellation failed. */
error?: string
}
CheckoutResponseResponse from POST /api/billing/checkout. Either checkoutUrl or updated is set.
interface CheckoutResponse {
/** Stripe Checkout URL — set when the user has no active subscription yet. */
checkoutUrl?: string
/** True when the user already had a subscription that was updated in place. */
updated?: boolean
/** Updated subscription metadata (only when `updated === true`). */
subscription?: {
expiresAt?: string
autoRenews?: boolean
}
}
LimitsItemPropsProps for <LimitsItem>.
interface LimitsItemProps {
/** Row content (typically a translated label + number). */
children: ReactNode
/**
* Whether this feature is included in the tier. When `false`, the row
* renders with a muted line-through and a dash glyph instead of the
* check icon. Defaults to `true`.
*/
included?: boolean
}
PricingPagePropsProps for <PricingPage />.
interface PricingPageProps<TLimits = unknown> {
/**
* Optional billing-period selector. Defaults to `'month'`. Pass `'year'`
* to render the yearly column. The component falls back to whatever the
* tier provides when the requested period is missing.
*/
period?: PricingTierPrice['period']
/**
* Optional render function for the tier-specific limits column. Defaults
* to a stacked checklist rendering numeric / boolean values with a green
* check glyph. Apps with rich limit shapes can supply a custom renderer.
*/
renderLimits?: (limits: TLimits) => React.ReactNode
/**
* Optional override for the page-level heading translation key.
* Defaults to `'billing.pricing.heading'`.
*/
headingKey?: string
/** Optional English fallback for the heading. Defaults to `'Choose your plan'`. */
headingDefault?: string
/**
* Optional sub-heading shown under the page heading. Pass `null` to
* suppress. Defaults to a translated "Pick the plan that fits…" line.
*/
subheadingKey?: string | null
/** English fallback for the sub-heading. */
subheadingDefault?: string
/**
* Optional className applied to the outer wrapper, useful when embedding
* the page in an existing layout.
*/
className?: string
/**
* Path the browser is sent to when an anonymous visitor clicks a paid
* tier's upgrade CTA. Defaults to `/login`. Set to `null` to disable
* the redirect (e.g. when the app handles the auth gate at a higher
* level via routing guards).
*/
unauthenticatedRedirect?: string | null
/**
* Optional tier key to highlight as "most popular" — receives the
* elevated card variant, a popular-badge in the header, and a
* primary-tone CTA. When omitted (default), the highest-priced tier
* with a real stripePriceId for the selected period is auto-selected.
* Pass `null` to disable highlighting entirely.
*/
popularTierKey?: string | null
/**
* Optional font-family stack applied inline to the page heading,
* tier names, and hero price. Defaults to a system-serif cascade
* (Georgia, Iowan Old Style, …) matching the flagship-app
* convention. Pass `null` to disable inline font and inherit from
* the theme.
*/
headlineFontFamily?: string | null
}
PricingTierEntryOne row of the public pricing table. Apps can declare any number of
period variants per tier (typically a month and year pair).
interface PricingTierEntry<TLimits = unknown> {
/** Stable slug used as a row key (`'free'`, `'pro'`, `'team'`). */
key: string
/** Display name shown in the page heading and CTA. */
name: string
/** Price variants for the tier. Order is presentation order. */
prices: PricingTierPrice[]
/** Tier-specific limits — render any/all of these on the comparison row. */
limits: TLimits
/** Whether the tier is billed per seat (rendered as a footnote). */
perSeat?: boolean
}
PricingTierPriceOne billing period offered for a tier — typically month or year —
with the human-readable price string and the Stripe price ID. The price
ID may be null for the free tier (no Stripe product).
interface PricingTierPrice {
/** Billing cadence. */
period: 'month' | 'year'
/** Display string (e.g. `'$19/mo'`, `'$190/yr'`, `'$0'`). */
price: string
/**
* Stripe Price ID used as the line item when the user clicks Upgrade.
* `null` for free tiers and during local dev when env vars are unset.
*/
stripePriceId: string | null
/** Optional savings tag on yearly variants (e.g. `'2 months free'`). */
savings?: string
}
PricingTiersResponseResponse envelope for GET /api/billing/tiers.
interface PricingTiersResponse<TLimits = unknown> {
/** Tiers ordered as the API returns them (typically free → pro → team). */
data: PricingTierEntry<TLimits>[]
}
BillingStatusBadge(props)Compact billing-status display for the user's account/settings page. Shows the current tier name and offers a cancel button on paid tiers.
function BillingStatusBadge(
props: BillingStatusBadgeProps,
): ReactElement<unknown, string | JSXElementConstructor<any>> | null
props — Component props.Returns: The rendered status badge.
LimitsItem(props)Single row in a tier's feature list — a check (or em-dash) icon
followed by the row label. Use inside <LimitsList>.
function LimitsItem({
children,
included = true,
}: LimitsItemProps): ReactElement<unknown, string | JSXElementConstructor<any>>
LimitsList(props)Container for a tier's feature list. Apps use this in their
renderLimits prop with <LimitsItem> children to get the polished
stacked-checklist layout (check icon prefix, muted/primary colors,
spacing) that matches the rest of <PricingPage />.
function LimitsList({
children,
}: {
children: ReactNode
}): ReactElement<unknown, string | JSXElementConstructor<any>>
PricingPage(props)Renders the public pricing page. Fetches /api/billing/tiers on mount
and lays out one card per tier with the price for the selected period
and a CTA that starts a Stripe Checkout session via
/api/billing/checkout. Tiers without a Stripe priceId (e.g. the free
tier or local-dev) render a disabled CTA so users still see the row.
The highest-priced paid tier is highlighted as "most popular" by
default — the card uses the elevated variant, the header carries a
star badge, and its CTA renders in the primary color. Apps that want
a different tier in the spotlight can pass popularTierKey (or
null to suppress).
function PricingPage(
props: PricingPageProps<TLimits>,
): ReactElement<unknown, string | JSXElementConstructor<any>>
props — Component props (see PricingPageProps).Returns: The rendered pricing page.
useBillingStatus()Fetch the signed-in user's current subscription state once on mount.
Returns 401 from the API when the user is not authenticated; consumers
should treat the absence of data as "anonymous → free tier".
function useBillingStatus(): UseHttpResult<BillingStatus<TLimits>>
Returns: Async-state for the user's billing status.
useCancelSubscription()Cancel the user's active subscription at the end of the current billing period. Returns the response from the bonded payment provider.
function useCancelSubscription(): BillingActionState<CancelResponse> & {
cancel: () => Promise<CancelResponse | null>
}
Returns: Async-state plus a cancel function.
usePricingTiers()Fetch the public pricing data once on mount. Useful inside a pricing/upgrade page where the tiers are needed before render.
function usePricingTiers(): UseHttpResult<PricingTiersResponse<TLimits>>
Returns: Async-state for the pricing tiers response.
useStartCheckout()Start a Stripe Checkout session for a given Stripe price ID. The
returned start(priceId) posts to /api/billing/checkout; the
response is either { checkoutUrl } (for new subscribers — redirect
the browser) or { updated: true } (for existing subscribers —
refresh the page).
function useStartCheckout(): BillingActionState<CheckoutResponse> & {
start: (priceId: string) => Promise<CheckoutResponse | null>
}
Returns: Async-state plus a start function.
Peer dependencies:
@molecule/app-http ^1.0.1@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1@molecule/app-ui-react ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-http@molecule/app-react@molecule/app-ui@molecule/app-ui-reactreactName collision: PricingPage is also exported by
@molecule/app-pricing-page-react (a tier-card grid with a monthly/yearly
toggle driven by usePricingTiers()). THIS package's <PricingPage> is the
entitlements-kit table with a renderLimits prop + compound
<LimitsList>/<LimitsItem> and a built-in <BillingStatusBadge> — import
from @molecule/app-billing-react when you are wiring
@molecule/api-entitlements. If you import both packages, alias one to avoid
the clash.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
/api/billing/tiers with name,
price, and per-tier limits — no empty table, no undefined cells./api/billing/checkout and the
page follows the returned checkout handoff (button is not a dead click).<BillingStatusBadge /> on the account screen shows the live tier, and its
cancel action updates the shown status after confirmation.Translation strings are provided by @molecule/app-locales-billing.