Skip to navigation

Derivations and Formatting

A derivation is a pure function from a hook’s data to a display model: (serverData, options) => ViewModel. The drop-ins render from these, and they are exported from @schematichq/schematic-components/elements so your own JSX renders from the same answers. Numbers, names, and dates arrive formatted for locale, the raw values sit beside them, and none of them assemble sentences. Where a line needs wording, it carries a kind with its formatted parts, so the words and their translation stay in your code.

The derivations

DerivationInputReturns
derivePlanManager({ company, featureUsage, creditBalances }, { locale, now?, showCredits?, showZeroPriceAsFree?, showWarningThresholdAsLimit? })useCompany, useFeatureUsage, useCreditBalances{ notice, plan, addOns, usageBased, planCredits, autoTopup, topUps, bundles, promotional, canChangePlan }
deriveIncludedFeatures(features, { locale, visibleFeatures?, showCredits?, showWarningThresholdAsLimit? })useFeatureUsageOne row per feature: featureId, name, description, icon, entitlement, usage, usageSummary, expiresAt, perLicenseCredits, hardLimit, tiers, source
deriveMeteredFeatures(features, { locale, visibleFeatures?, showCredits?, showWarningThresholdAsLimit? })useFeatureUsageOne row per event or trait feature: featureId, name, description, icon, headline, limit, resetsAt, meter, priceDetails, hardLimit, canAddMore, hasUsageByUser, unit, source
deriveCreditUsage(balances, { locale, visibleCredits? })useCreditBalancesOne row per credit: creditId, name, description, icon, remaining, purchasable, ledger, composition, unit, source
deriveCheckout({ catalog, cart, checkout?, company?, featureUsage?, isPricing?, display?, locale })useCatalog, useCompany, useCheckout, useFeatureUsage{ currency, period, plans, customPlan, downgradeContact, addOns, usage, addOnUsage, bundles, bundleMode, autoTopups, lines, totals, ... }, the rows and summary the Checkout dialog renders
derivePricingTable(view, { locale, period?, currency?, company?, now?, showCredits?, showZeroPriceAsFree?, showWarningThresholdAsLimit? })useCatalog, and useCompany when signed in{ source, periods, period, currencies, currency, plans, addOns, customPlan, savings, trialDaysLeft, heldAddOnIds }; each card has id, name, description, price, priceId, period, credits, entitlements, action, active, trialDays, source
checkoutSelectionFor(table, card, kind)A derivePricingTable result, one of its cards, and "plan" or "addOn"The CheckoutSelection for the card’s action: the plan with its period and currency, or the add-ons the company would hold after it
deriveUpcomingInvoice(invoice, { locale, format? })useUpcomingInvoiceamountDueMinor, amountDueText, subtotalMinor, subtotalText, currency, dueAt, balanceApplied, balanceRemaining, discounts[]
derivePaymentMethods(methods, { locale, now? })usePaymentMethods{ rows, current, others, monthsToExpiration, expiryWarning }
deriveInvoiceList(page, { locale, format? })useInvoices{ rows, count, hasMore }; each row has id, date, dateText, amountMinor, currency, amountText, isCredit, status, url
deriveUnsubscribe(company, featureUsage, { locale, now? })useCompany, useFeatureUsage{ canUnsubscribe, accessEndsOn, period, plan, addOns, total }

Most rows keep the server’s row on source, so a component that needs a field the model leaves out reads it there instead of abandoning the derivation.

Plan Manager

derivePlanManager lays the company out the way the Plan Manager component does. notice is at most one of four kinds, checked in order: trial with endsIn counted down in days, hours, or minutes; canceled with the plan name and date; customPlanBilling for a custom plan’s pending invoice; and scheduledDowngrade with both plan names. plan.price is { kind: "amount", amount, period }, { kind: "usageBased" } for a free plan with usage-based features, or { kind: "free" } when showZeroPriceAsFree is set. usageBased holds each feature with a price behavior, planCredits sums the plan’s grants of each credit, and topUps, bundles, and promotional group the company’s auto top-up, purchased, and free grants. canChangePlan is false while a custom plan waits for payment to activate.

Included and metered features

deriveIncludedFeatures describes what each entitlement gives. entitlement is one of units, perUnit, perPackage, tierUpTo, tierUnlimited, perUse, or unlimited, each with its amounts and unit names formatted, or null for a feature with no entitlement line, such as a boolean feature. usage is a list of segments (unitPricePerPeriod, used, cost, resets) that the Included Features component joins with a bullet, and usageSummary stands in when that list is empty.

deriveMeteredFeatures keeps the event and trait features and gives each the parts of a usage card. meter has value, total, percent from 0 to 100, valueText, totalText, and a tone of ok, warning, critical, tier, or overage. A pay-as-you-go or credit-burndown feature has no meter. hasUsageByUser marks the event-based rows that useFeatureUserUsage can break down by user.

Credits

deriveCreditUsage gives each credit its remaining amount and unit name, and a ledger of its live grants, newest first. Each ledger row has a kind of plan, bundle, autoTopup, or promotional for any other grant, the granted amount with the credit’s name for it in item, its createdAt date, a date that says when the grant resets or expires, and the server’s grant on source. composition spells out a per-license grant, such as 12 seats at 10 credits each.

Pricing table

Selection state such as the period toggle stays in your component; derivePricingTable takes the selection and returns what the catalog offers. periods lists the periods the catalog prices, months before quarters before years, and currencies the currencies, lowercase. period and currency are the ones you asked for when the catalog offers them, else the subscription’s period and the catalog’s default currency, else the first; period is null when nothing recurs. Pass company from useCompany when signed in, so the current plan is current only in the period it bills in and the table opens on that period.

plans and addOns hold a card each, priced in that period and currency. A card with prices but none in the currency is left out, and a recurring one with none in the period; a one-time add-on is priced once. price is { kind: "amount", amount, period }, { kind: "usageBased" } for a plan priced at zero with a priced feature, or { kind: "free" } with showZeroPriceAsFree. priceId is the price the card shows, for a checkout. credits lists the included credits the catalog shows, one line per credit; a credit granted per license and to the company outright reads as the per-license line with the company’s share in plus. savings holds what the company’s current plan saves by period against paying monthly, as a formatted percentage, and is empty in the public catalog. entitlements gives each feature a text with a kind of name, unlimited, units, perUnit, perUse, or upTo, a detail of overage, tiered, or null, its tiers, and its hardLimit.

action is null in the public catalog. In the company’s view it is one of current; choose with upgrade for a plan above the current one; remove or change for a held add-on; overLimit with the violations; downgradeBlocked with the account’s text and url; or unavailable when the company cannot check out. active marks the company’s plan in the period shown, and trialDaysLeft counts down its trial. customPlan is the catalog’s custom-plan call to action, with priceText, ctaText, and ctaUrl, or null.

Checkout

deriveCheckout turns the catalog, the cart and the checkout the server last priced into what the Checkout dialog shows: the plan and add-on rows with their standing, the quantity, bundle and auto top-up rows, and the summary’s lines and totals from checkout.priceSnapshot. The cart is its own small model: initialCart({ catalog, company, config }) builds it from what the company holds and a CheckoutConfig, cartReducer applies each choice, and checkoutSelections(cart, catalog, company) turns it into the CheckoutSelections that useCheckout().setSelections writes, picking the price ids for the period and currency. planSteps derives the steps the cart has, and stepOfProblem(problem, { cart, catalog }) names the step that can fix a problem, which is how a refused finalize lands the customer in the right place. lines are the cart’s rows at the catalog’s prices, the plan, each quantity bought ahead at its unit price, the add-ons and the bundles, and totals the snapshot’s figures formatted, so a summary can show what makes up the total the server priced. The autoTopups rows name the credit and price one top-up on costText, which the catalog’s own autoTopups do not. Checkout API describes the resource these write to.

Upcoming bill

deriveUpcomingInvoice formats the parts without doing the arithmetic, because the server sends how much balance the invoice consumes and how much survives. balanceApplied comes back negative, as a deduction, and null when none of the balance applies. Each entry in discounts has couponName, code, a kind of percent or amount, valueText such as “20%” or “$5.00”, and months for a discount that repeats, with the raw percentOff, amountOffMinor, and duration beside them.

Invoices

deriveInvoiceList formats each row the way the Invoices component does: the due date, or the created date when there is none, in the locale’s long form such as “March 1, 2026”; the amount as absolute text with isCredit carrying the sign; currency upper-cased; and url reduced to null unless it is an http or https link. date is null rather than an Invalid Date when the timestamp is unusable, and dateText is then empty.

Payment methods

derivePaymentMethods lays the list out the way the Payment Methods component does. current is the default, or null when there is none, and others is every other row, or all of them when there is no default.

expiryWarning is soon when the default card has fewer than four months left, expired once its month has arrived, and none otherwise. Months count by calendar month and ignore the day, so a card expiring in August is two months away anywhere in June. now fixes the moment they are counted from, for a test or a server render.

Each row has id, externalId, kind, label, icon, brand, type, last4, expiresShort, monthsToExpiration, expiry, isDefault, and canRemove. kind is card, bank, wallet, or other. icon names the Schematic icon font glyph: visa, mastercard, amex, or credit for other cards, bank for a US bank account, the wallet’s own mark, and generic-payment otherwise. expiresShort is the card’s “8/27”.

label is either { key }, copy to translate such as paymentMethodsCardEndingIn for “Card ending in”, or { text }, a value the provider supplied such as a bank’s name or the email behind a Link account. That keeps the translatable words in your strings:

const { current, others } = derivePaymentMethods(data, { locale: "en-US" });
const name = (row: PaymentMethodRow) =>
row.label.key !== undefined ? t(row.label.key) : row.label.text;

Cancellation

deriveUnsubscribe answers what a cancel dialog needs. canUnsubscribe is true while there is a subscription with no cancellation date. accessEndsOn is the cancellation date, else the next bill date, else today. plan, addOns, and total are what cancelling stops charging for, with total the recurring cost per period.

Overriding one field

deriveInvoiceList and deriveUpcomingInvoice take format, which replaces one formatter at a time and receives the raw value. The amount formatter receives the signed amount, unlike deriveInvoiceList’s default text, which is absolute:

deriveInvoiceList(page, {
locale: "en-US",
format: {
date: (date, locale) => date.toLocaleDateString(locale),
amount: (amountMinor, currency, locale) => myMoney(amountMinor, currency),
},
});

Formatting helpers

HelperDoes
formatCurrency(amountMinor, currency, locale, options?)Currency with zero-decimal handling and significant-digit switching for small unit prices. { preserveSubUnitPrecision: false } rounds to the currency’s usual precision.
formatNumber(value, locale, options?)Grouped numbers, with Intl.NumberFormat options passed through
formatPercent(fraction, locale)”50%”, or “12.5%” for a fractional percentage
formatDate(date, locale, options?), formatShortDate(date, locale), formatShortDateRange(start, end, locale)A Date as “August 21, 2026” or “8/21”, or a range as “8/21 – 9/20”. Empty for an Invalid Date.
formatMonthYear(month, year, locale)A card’s expiry as the locale writes a month and year
formatConsumptionRate(rate, locale)Rates readable down to ten decimal places
plural(locale, count, forms)Picks from { one?, other, … } by the locale’s plural category
featureName({ name, singularName?, pluralName? }, count?, locale?)Singular or plural feature or credit name, using configured forms first
resolveLocale(locale?), viewerLocale()The tag or en-US when it is absent or Intl rejects it; the browser language, if usable
httpStatus(error)The HTTP status behind a failed billing read, or undefined for anything that was not an HTTP response

usageLimit and currentTier read a single FeatureUsage row directly, and pricePeriod, tierRanges, isTieredPrice, and unitPrice read its price, for a component that works from the rows rather than a derivation. <Icon name={row.icon} /> draws a feature’s or credit’s icon the way the drop-ins do: the glyph for a schematic-icons name, and anything else, such as an emoji, as text.

Tests travel with them

The drop-ins render from the same derivations, and their test suites check the derivations directly, so a rendering rule that changes is caught before it reaches a page. Pin @schematichq/schematic-components to a minor to take those changes deliberately.