Cobrança

Plano e uso para um produto de IA, no estilo de Cursor, Claude e Vercel. Um medidor de uso dividido por modelo que projeta o fim do ciclo e avisa antes de os créditos acabarem, um gráfico diário que você pode percorrer, um limite de gastos com alertas que você pode pré-visualizar no medidor, mudanças de plano com proporcional exato, um formulário de cartão com validação real e faturas que baixam como PDF.

O Billing é a seção Plano e uso das configurações de um produto de IA. Ele se encaixa em uma seção do SettingsShell e cobre o motivo pelo qual as pessoas vão a essa página: quanto usaram, se vai durar, o que pagam e como alterar.

O medidor de uso se preenche por modelo, com uma extensão mais clara mostrando onde o ciclo provavelmente vai terminar no ritmo dos últimos 7 dias. Quando isso ultrapassa os créditos incluídos, uma nota discreta diz quando: "Neste ritmo, seus créditos acabam por volta de 24 de out., 4 dias antes de serem renovados", com formas de ativar o uso extra ou ver os planos. Abaixo, um gráfico de barras diário construído sobre o Chart cobre o ciclo inteiro: dias passados, hoje com força total, o resto do ciclo como barras projetadas esmaecidas no seu ritmo recente e uma linha tracejada para o ritmo uniforme que duraria o ciclo todo. Passe o mouse ou foque nele e use as setas do teclado para ler qualquer dia.

Os limites são um rascunho, como em toda outra seção de configurações. Arraste o limite de alerta ou ative o uso extra e os marcadores se movem nos medidores imediatamente, o medidor de uso extra desliza para dentro e a barra de salvar sobe. Um limite menor do que o que já foi gasto neste ciclo é recusado informando o valor.

Os upgrades mostram o proporcional exato antes de qualquer cobrança: o novo plano pelos dias restantes, os dias não usados do atual e "Você pagará $13.33 hoje". Os downgrades esperam até o fim do período e listam o que deixa de existir. Cancelar é uma confirmação honesta, sem ofertas, o plano continua ativo até o fim do período e Keep Pro desfaz o cancelamento até lá.

O formulário do cartão formata os números enquanto você digita sem mover o cursor, valida-os com Luhn, detecta a bandeira e valida a validade e o código de segurança. As faturas têm badges de status, Pay now para as que falharam e são baixadas como arquivos PDF de verdade.

  1. Adicione o registro Pro ao components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Adicione seu token

    Crie um token na sua página de conta e coloque-o em .env.local como HEXTAUI_PRO_TOKEN.

  3. Adicione o bloco

    pnpm dlx shadcn@latest add @hextaui-pro/billing

Conecte à sua API

BillingSettings mostra os dados que você passa e chama você de volta a cada mudança. Retorne ou lance um erro em um callback: a mensagem lançada aparece no diálogo que a solicitou, e nada muda até você passar novas props.

"use client"

import * as React from "react"

import { SettingsSection, SettingsShell } from "../settings/settings"
import {
  BillingSettings,
  BillingSettingsSkeleton,
  type BillingSettingsProps,
} from "@/components/blocks/billing/billing-settings"

type BillingData = Pick<
  BillingSettingsProps,
  "plans" | "subscription" | "usage" | "spendLimit" | "paymentMethod" | "invoices" | "today"
>

async function post(url: string, body: unknown = {}) {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  })
  if (!response.ok) {
    const { message } = await response.json().catch(() => ({ message: "" }))
    throw new Error(message || "Try again in a moment.")
  }
}

export function BillingPage() {
  const [data, setData] = React.useState<BillingData | null>(null)
  const [failed, setFailed] = React.useState(false)

  const load = React.useCallback(async () => {
    setFailed(false)
    try {
      const response = await fetch("/api/billing")
      if (!response.ok) throw new Error()
      setData(await response.json())
    } catch {
      setFailed(true)
    }
  }, [])

  React.useEffect(() => {
    void load()
  }, [load])

  const then = (url: string) => async (body?: unknown) => {
    await post(url, body)
    await load()
  }

  return (
    <SettingsShell sections={[{ id: "billing", label: "Plan & usage" }]}>
      <SettingsSection
        id="billing"
        status={failed ? "error" : data ? "ready" : "loading"}
        skeleton={<BillingSettingsSkeleton />}
        onRetry={load}
      >
        {data ? (
          <BillingSettings
            {...data}
            customer={{ name: "Mia Chen", email: "[email protected]" }}
            onChangePlan={then("/api/billing/plan")}
            onCancelPlan={then("/api/billing/cancel")}
            onResumePlan={then("/api/billing/resume")}
            onSpendLimitChange={then("/api/billing/limit")}
            onPayInvoice={(invoice) => then("/api/billing/pay")({ id: invoice.id })}
          />
        ) : null}
      </SettingsSection>
    </SettingsShell>
  )
}

Atualizações de cartão

onUpdatePaymentMethod recebe os dígitos limpos, a validade, o código de segurança e a bandeira assim que o formulário passa nas verificações. Lance um erro para mostrar uma recusa no diálogo. Em produção, entregue os dados à tokenização do seu provedor de pagamentos e nunca os armazene você mesmo.

"use client"

import * as React from "react"

import { BillingSettings, type BillingSettingsProps } from "@/components/blocks/billing/billing-settings"

export function BillingWithStripe(props: BillingSettingsProps) {
  return (
    <BillingSettings
      {...props}
      onUpdatePaymentMethod={async (card) => {
        const response = await fetch("/api/billing/card", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(card),
        })
        if (response.status === 402) {
          throw new Error("Your bank declined this card. Try another card.")
        }
        if (!response.ok) throw new Error("We couldn’t save that card. Try again.")
      }}
    />
  )
}

Seus próprios arquivos de fatura

As faturas são baixadas como PDFs gerados no navegador a partir dos dados da fatura. Retorne um Blob de onDownloadInvoice para servir o seu próprio arquivo, ou dê um href à fatura.

"use client"

import * as React from "react"

import { BillingSettings, type BillingSettingsProps } from "@/components/blocks/billing/billing-settings"

export function BillingWithServerInvoices(props: BillingSettingsProps) {
  return (
    <BillingSettings
      {...props}
      seller="Acme Labs, Inc."
      onDownloadInvoice={async (invoice) => {
        const response = await fetch(`/api/invoices/${invoice.id}/pdf`)
        if (!response.ok) throw new Error("That invoice isn’t ready yet.")
        return response.blob()
      }}
    />
  )
}

Anatomia

As partes que você compõe, de fora para dentro.

ParteDescrição
BillingSettingsO conteúdo da seção: resumo do plano, medidores de uso e o Chart diário, limites, planos, pagamento, faturas e cancelamento, com seus diálogos.
BillingSettingsSkeletonUm placeholder de carregamento com o formato da seção. Passe-o para o skeleton do SettingsSection.
prorateO cálculo proporcional por trás do diálogo de upgrade, para o seu servidor ou testes.
forecastUsageA projeção por trás do medidor e da nota de esgotamento.
createInvoicePdfGera o PDF da fatura como um Blob.

BillingSettings

Os preços estão em centavos e as datas são dias ISO como 2026-10-28. Omita um callback para ocultar a sua ação.

PropTipoPadrão
plans{ id, name, price, included, description?, features }. Features são listas completas por plano, para que um downgrade possa listar o que está faltando. Um plano com price 0 é o plano para o qual as pessoas passam quando cancelam.
BillingPlan[]–
subscription{ planId, periodStart, periodEnd, status?, cancelAtPeriodEnd?, scheduledPlanId? }.
BillingSubscription–
usage{ models: { id, name, used }[], daily: { date, used }[], extraRate, unit? }. extraRate é o valor em centavos por unidade além do que está incluído. unit tem como padrão "credits".
BillingUsage–
spendLimit{ enabled, limit, alertAt }. limit está em centavos e alertAt é uma porcentagem.
BillingSpendLimit–
paymentMethod{ brand, last4, expMonth, expYear, name? }.
BillingPaymentMethod | null–
invoices{ id, number, date, description, amount, status, href?, lines?, paidWith? }. status é paid, open, failed, refunded ou void.
BillingInvoice[][]
todayO dia que o seu servidor considera hoje. Usado para o proporcional, as projeções e a validade do cartão, para que servidor e navegador concordem.
stringthe last daily date
currencyQualquer código de moeda ISO.
string"USD"
sellerO nome no topo das faturas geradas.
string"Hexta"
customerImpresso sob Bill to nas faturas geradas.
{ name?, email? }–
onChangePlanChamado com { planId, when, amountDue }. when é "now" para upgrades e "period_end" para downgrades.
(change) => void | Promise–
onCancelPlanCancelar ao fim do período.
() => void | Promise–
onResumePlanDesfaz um cancelamento ou um downgrade agendado.
() => void | Promise–
onSpendLimitChangeSalva os limites. Retorne { limit: message } para mostrar um erro de campo.
(limit) => void | errors | Promise–
onUpdatePaymentMethodChamado com { number, expMonth, expYear, cvc, name, brand } quando o formulário é válido. Lance um erro para mostrar uma recusa.
(card) => void | Promise–
onPayInvoiceAdiciona Pay now às faturas em aberto e com falha.
(invoice) => void | Promise–
onDownloadInvoiceRetorne um Blob para baixar o seu próprio arquivo, ou nada se você mesmo tratou o download. Sem isso, um PDF é gerado a partir da fatura.
(invoice) => Blob | void | Promise–

prorate

Retorna { kind, amountDue, charge, credit, daysLeft, totalDays, effectiveDate, nextBillingDate }.

PropTipoPadrão
optionsOs upgrades cobram a diferença de preço pelos dias restantes, arredondada uma vez. Os downgrades não cobram nada e começam em periodEnd. Sair de um plano gratuito cobra um mês inteiro a partir de hoje.
{ from, to, periodStart, periodEnd, today }–
TeclaAção
←→Com o gráfico diário em foco, move o tooltip para o dia anterior ou o seguinte.
EnterNo formulário do cartão, confira os dados e salve.
⌘SSalva os limites alterados. Ctrl+S no Windows e no Linux.
  • Os medidores usam role meter com um texto de valor que inclui a projeção e o limite de alerta, e a divisão por modelo é uma lista de verdade.
  • O gráfico diário é uma figure chamada "Uso diário neste ciclo", descrita por um resumo com o total, a média diária recente, o dia de maior movimento e o ritmo uniforme. Uma tabela visualmente oculta lista todos os dias, com os dias projetados marcados como estimativas.
  • Mudanças de plano, cancelamentos, atualizações de cartão e downloads são anunciados de forma polite. As falhas aparecem no diálogo que as solicitou, como um alerta, e o diálogo permanece aberto com o que você digitou.
  • Os campos do cartão usam os tokens padrão de autocomplete, para que navegadores e gerenciadores de senhas possam preenchê-los. O primeiro campo inválido recebe o foco.
  • Os inputs têm 16px em telas de toque para que o iOS não dê zoom, e os botões crescem para 44px.
  • Números e datas são formatados em inglês com fusos horários fixos, para que o servidor e o navegador renderizem o mesmo texto.

Construído com

Os componentes gratuitos do HextaUI de que Billing é feito. Cada um é instalado separadamente.

Código

4 arquivos, adicionados a components/blocks/billing.