請求

Cursor、Claude、Vercelのスタイルによる、AIプロダクト向けのプランと使用量です。モデルごとに分かれ、サイクル終了時を予測してクレジット切れの前に警告する使用量メーター、ドラッグで確認できる日別チャート、メーター上でプレビューできるアラートつきの支出上限、正確な日割り計算によるプラン変更、実際のバリデーションを備えたカードフォーム、PDFでダウンロードできる請求書を備えています。

Billingは、AIプロダクトの設定にあるプランと使用量のセクションです。SettingsShellのセクションに組み込め、そのページを訪れる人の目的をカバーします。どれだけ使ったか、足りるのか、いくら支払うのか、どう変更するのか、という点です。

使用量メーターはモデルごとに埋まり、直近7日のペースでサイクルがおそらくどこで終わるかを、薄い延長部分で示します。それが含まれるクレジットを超える場合は、控えめなメモが時期を伝えます: 「At this pace you’ll run out around Oct 24, 4 days before your credits reset」。追加使用量をオンにする方法やプランを見る方法も示されます。その下には、Chartで作られた日別の棒グラフがサイクル全体を示します。過去の日、濃く表示される今日、直近のペースで予測された残りのサイクルを示す薄い棒、サイクルを持たせられる均等なペースを示す破線です。ホバーまたはフォーカスして矢印キーを使えば、任意の日を読み取れます。

上限は、他の設定セクションと同様に下書きです。アラートのしきい値をドラッグするか追加使用量をオンにすると、メーターのマーカーがすぐに動き、追加使用量のメーターがスライドして現れ、保存バーが浮かび上がります。このサイクルですでに使った額より低い上限は、その金額とともに拒否されます。

アップグレードでは、請求される前に正確な日割り計算が表示されます。残り日数分の新しいプラン、現在のプランの未使用日数、そして「You’ll pay $13.33 today」です。ダウングレードは期間の終了まで待ち、失われるものを一覧します。キャンセルは、特典の提示なしの正直な確認が1回だけで、プランは期間が終わるまで有効なままで、Keep Proでそれまで取り消せます。

カードフォームは、カーソルを動かさずに入力中の数字を整形し、Luhnアルゴリズムで検証し、ブランドを検出し、有効期限とセキュリティコードを検証します。請求書にはステータスバッジがあり、失敗したものにはPay nowが付き、本物のPDFファイルとしてダウンロードできます。

  1. Proレジストリをcomponents.jsonに追加する

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. トークンを追加する

    アカウントページでトークンを作成し、.env.local に HEXTAUI_PRO_TOKEN として設定してください。

  3. ブロックを追加する

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

APIに接続する

BillingSettingsは渡されたデータを表示し、変更のたびにコールバックを呼び出します。コールバックから値を返すか、エラーを投げてください。投げたメッセージは要求元のダイアログに表示され、新しいプロップを渡すまで何も変わりません。

"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>
  )
}

カードの更新

onUpdatePaymentMethodは、フォームがチェックを通過すると、正規化された数字、有効期限、セキュリティコード、ブランドを受け取ります。エラーを投げると、拒否がダイアログに表示されます。本番環境では、詳細を決済プロバイダーのトークン化に渡し、自分で保存しないでください。

"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.")
      }}
    />
  )
}

独自の請求書ファイル

請求書は、請求書のデータからブラウザー内で作成されたPDFとしてダウンロードされます。独自のファイルを提供するには、onDownloadInvoiceからBlobを返すか、請求書にhrefを指定してください。

"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()
      }}
    />
  )
}

構造

外側から内側へ組み合わせるパーツ。

パーツ説明
BillingSettingsセクションの内容。プランの概要、使用量メーターと日別Chart、上限、プラン、支払い、請求書、キャンセルと、それぞれのダイアログです。
BillingSettingsSkeletonセクションの形をした読み込みプレースホルダー。SettingsSectionのskeletonに渡します。
prorateアップグレードダイアログの背後にある日割り計算。サーバーやテストで使えます。
forecastUsageメーターの背後にある予測と、使い切りのメモ。
createInvoicePdf請求書のPDFをBlobとして作成します。

BillingSettings

価格はセント単位で、日付は 2026-10-28 のようなISOの日付です。コールバックを省略すると、そのアクションは非表示になります。

プロパティ型デフォルト
plans{ id, name, price, included, description?, features }。featuresはプランごとの完全なリストなので、ダウングレード時に失われるものを一覧できます。priceが0のプランは、キャンセル時に移行するプランです。
BillingPlan[]–
subscription{ planId, periodStart, periodEnd, status?, cancelAtPeriodEnd?, scheduledPlanId? }。
BillingSubscription–
usage{ models: { id, name, used }[], daily: { date, used }[], extraRate, unit? }。extraRateは、含まれる分を超えた1単位あたりのセント数です。unitのデフォルトは "credits" です。
BillingUsage–
spendLimit{ enabled, limit, alertAt }。limitはセント単位で、alertAtはパーセンテージです。
BillingSpendLimit–
paymentMethod{ brand, last4, expMonth, expYear, name? }。
BillingPaymentMethod | null–
invoices{ id, number, date, description, amount, status, href?, lines?, paidWith? }。statusはpaid、open、failed、refunded、voidのいずれかです。
BillingInvoice[][]
todayサーバーが今日とみなす日。日割り計算、予測、カードの有効期限に使われ、サーバーとブラウザーで結果が一致します。
stringthe last daily date
currency任意のISO通貨コード。
string"USD"
seller生成された請求書の上部に表示される名前。
string"Hexta"
customer生成された請求書のBill toの下に印刷されます。
{ name?, email? }–
onChangePlan{ planId, when, amountDue } とともに呼ばれます。whenは、アップグレードでは "now"、ダウングレードでは "period_end" です。
(change) => void | Promise–
onCancelPlan期間の終了時にキャンセルします。
() => void | Promise–
onResumePlanキャンセル、または予定されているダウングレードを取り消します。
() => void | Promise–
onSpendLimitChange上限を保存します。フィールドエラーを表示するには、{ limit: message } を返します。
(limit) => void | errors | Promise–
onUpdatePaymentMethodフォームが有効になると、{ number, expMonth, expYear, cvc, name, brand } とともに呼ばれます。エラーを投げると、拒否が表示されます。
(card) => void | Promise–
onPayInvoice未払いおよび失敗した請求書にPay nowを追加します。
(invoice) => void | Promise–
onDownloadInvoice独自のファイルをダウンロードさせるにはBlobを返し、自分で処理した場合は何も返しません。指定しない場合、請求書からPDFが作成されます。
(invoice) => Blob | void | Promise–

prorate

{ kind, amountDue, charge, credit, daysLeft, totalDays, effectiveDate, nextBillingDate } を返します。

プロパティ型デフォルト
optionsアップグレードは、残り日数分の価格差を1回だけ丸めて請求します。ダウングレードは請求なしで、periodEndから始まります。無料プランから移行する場合は、今日から1か月分が請求されます。
{ from, to, periodStart, periodEnd, today }–
キーアクション
←→日別チャートにフォーカスがあるとき、ツールチップを前の日または次の日に移動します。
Enterカードフォームで、詳細を確認して保存します。
⌘S変更した上限を保存します。WindowsとLinuxではCtrl+Sです。
  • メーターはrole meterを使い、予測とアラートのしきい値を含む値のテキストを持ちます。モデルごとの内訳は実際のリストです。
  • 日別チャートは「Daily usage this cycle」という名前のfigureで、合計、最近の1日平均、最も利用が多かった日、均等なペースを含む要約で説明されます。視覚的に非表示のテーブルがすべての日を一覧し、予測された日は推定値として示されます。
  • プランの変更、キャンセル、カードの更新、ダウンロードはpoliteに読み上げられます。失敗は、要求元のダイアログにalertとして表示され、ダイアログは入力内容を保ったまま開いたままになります。
  • カードのフィールドには標準のautocompleteトークンを使用するため、ブラウザーやパスワードマネージャーが入力できます。最初の無効なフィールドにフォーカスが移ります。
  • タッチ画面では入力欄が16pxになりiOSでズームされず、ボタンは44pxに大きくなります。
  • 数値と日付は、固定されたタイムゾーンで英語表記にフォーマットされるため、サーバーとブラウザーで同じテキストが描画されます。

使用技術

Billing を構成する無料のHextaUIコンポーネントです。それぞれ単独でインストールできます。

コード

4 個のファイルを components/blocks/billing に追加しました。