{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "billing",
  "title": "Billing",
  "description": "Provider-neutral plans, subscription, portal, seats, cancellation, restoration, and usage settings with Stripe and Polar adapters.",
  "dependencies": [
    "@better-auth-ui/core@latest",
    "@better-auth-ui/react@latest",
    "cn",
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "badge",
    "button",
    "card",
    "field",
    "input",
    "progress",
    "select",
    "spinner"
  ],
  "files": [
    {
      "path": "examples/start-shadcn-example/src/components/auth/billing/billing-settings.tsx",
      "content": "\"use client\"\n\nimport {\n  type BillingAdapter,\n  type BillingInterval,\n  type BillingPlan,\n  type BillingScope,\n  followBillingAction\n} from \"@better-auth-ui/core/plugins/billing\"\nimport { useAuth, useAuthPlugin, useSession } from \"@better-auth-ui/react\"\nimport {\n  useBillingCheckout,\n  useBillingPlans,\n  useBillingPortal,\n  useBillingState,\n  useCancelBillingSubscription,\n  useRestoreBillingSubscription,\n  useUpdateBillingSeats\n} from \"@better-auth-ui/react/plugins/billing\"\nimport { Check, CreditCard, ExternalLink, RotateCcw, X } from \"lucide-react\"\nimport { useState } from \"react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogMedia,\n  AlertDialogTitle\n} from \"@/components/ui/alert-dialog\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from \"@/components/ui/card\"\nimport { Field, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Progress } from \"@/components/ui/progress\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue\n} from \"@/components/ui/select\"\nimport { Spinner } from \"@/components/ui/spinner\"\nimport { billingPlugin } from \"@/lib/auth/billing-plugin\"\nimport { cn } from \"cn\"\n\ntype SubscriptionAction = \"cancel\" | \"restore\"\n\nexport type BillingSettingsProps = {\n  adapter: BillingAdapter\n  scope: BillingScope\n  className?: string\n}\n\nconst formatPrice = (amount: number, currency: string) => {\n  const fractionDigits =\n    new Intl.NumberFormat(undefined, {\n      style: \"currency\",\n      currency\n    }).resolvedOptions().maximumFractionDigits ?? 2\n  const divisor = 10 ** fractionDigits\n\n  return new Intl.NumberFormat(undefined, {\n    style: \"currency\",\n    currency,\n    maximumFractionDigits: amount % divisor === 0 ? 0 : fractionDigits\n  }).format(amount / divisor)\n}\n\nconst availableIntervals = (plans: BillingPlan[]) =>\n  Array.from(\n    new Set(plans.flatMap((plan) => plan.prices.map((price) => price.interval)))\n  )\n\nfunction PlanCard({\n  plan,\n  interval,\n  currentPlanId,\n  isPending,\n  onChoose\n}: {\n  plan: BillingPlan\n  interval: BillingInterval\n  currentPlanId?: string\n  isPending: boolean\n  onChoose: (plan: BillingPlan, priceId: string) => void\n}) {\n  const { localization } = useAuthPlugin(billingPlugin)\n  const price = plan.prices.find((entry) => entry.interval === interval)\n  if (!price) return null\n  const isCurrent = currentPlanId === plan.id\n  const suffix =\n    price.interval === \"month\"\n      ? localization.perMonth\n      : price.interval === \"year\"\n        ? localization.perYear\n        : localization.oneTime\n\n  return (\n    <Card\n      className={cn(\n        \"relative h-full\",\n        plan.highlighted && \"border-primary/50 bg-primary/5\"\n      )}\n    >\n      <CardHeader>\n        <div className=\"flex items-start justify-between gap-3\">\n          <div className=\"flex min-w-0 flex-col gap-1\">\n            <CardTitle>{plan.name}</CardTitle>\n            {plan.description && (\n              <CardDescription>{plan.description}</CardDescription>\n            )}\n          </div>\n          {plan.highlighted && <Badge>{localization.popular}</Badge>}\n        </div>\n      </CardHeader>\n      <CardContent className=\"flex flex-1 flex-col gap-4\">\n        <div>\n          <span className=\"text-2xl font-semibold tracking-tight\">\n            {formatPrice(price.amount, price.currency)}\n          </span>\n          <span className=\"ml-1 text-xs text-muted-foreground\">{suffix}</span>\n        </div>\n        {plan.features?.length ? (\n          <ul className=\"flex flex-col gap-2 text-sm\">\n            {plan.features.map((feature) => (\n              <li key={feature} className=\"flex items-start gap-2\">\n                <Check className=\"mt-0.5 size-4 shrink-0 text-emerald-600\" />\n                <span>{feature}</span>\n              </li>\n            ))}\n          </ul>\n        ) : null}\n      </CardContent>\n      <CardFooter>\n        <Button\n          className=\"w-full\"\n          variant={isCurrent ? \"outline\" : \"default\"}\n          disabled={isCurrent || isPending}\n          onClick={() => onChoose(plan, price.id)}\n        >\n          {isCurrent ? localization.currentPlan : localization.choosePlan}\n        </Button>\n      </CardFooter>\n    </Card>\n  )\n}\n\nfunction SeatsEditor({\n  seats,\n  isPending,\n  onSave\n}: {\n  seats: number\n  isPending: boolean\n  onSave: (seats: number) => void\n}) {\n  const { localization } = useAuthPlugin(billingPlugin)\n  const [value, setValue] = useState(seats)\n\n  return (\n    <div className=\"flex items-end gap-2\">\n      <Field className=\"max-w-40\">\n        <FieldLabel htmlFor=\"billing-seats\">{localization.seats}</FieldLabel>\n        <Input\n          id=\"billing-seats\"\n          type=\"number\"\n          min={1}\n          value={value}\n          onChange={(event) =>\n            setValue(Math.max(1, event.target.valueAsNumber || 1))\n          }\n        />\n      </Field>\n      <Button\n        size=\"sm\"\n        variant=\"outline\"\n        disabled={isPending || value === seats}\n        onClick={() => onSave(value)}\n      >\n        {isPending && <Spinner />}\n        {localization.updateSeats}\n      </Button>\n    </div>\n  )\n}\n\nexport function BillingSettings({\n  adapter,\n  scope,\n  className\n}: BillingSettingsProps) {\n  const { localization } = useAuthPlugin(billingPlugin)\n  const plans = useBillingPlans(adapter, scope)\n  const state = useBillingState(adapter, scope)\n  const checkout = useBillingCheckout(adapter, scope)\n  const portal = useBillingPortal(adapter, scope)\n  const cancelSubscription = useCancelBillingSubscription(adapter, scope)\n  const restoreSubscription = useRestoreBillingSubscription(adapter, scope)\n  const updateSeats = useUpdateBillingSeats(adapter, scope)\n  const [interval, setInterval] = useState<BillingInterval>(\"month\")\n  const [action, setAction] = useState<SubscriptionAction>()\n  const subscription = state.data?.subscription\n  const intervals = availableIntervals(plans.data ?? [])\n  const resolvedInterval = intervals.includes(interval)\n    ? interval\n    : (intervals[0] ?? \"month\")\n  const isActionPending =\n    cancelSubscription.isPending || restoreSubscription.isPending\n\n  const handleAction = () => {\n    if (!subscription || !action) return\n    const mutation =\n      action === \"cancel\" ? cancelSubscription : restoreSubscription\n    mutation.mutate(subscription.id, {\n      onSuccess: (result) => {\n        setAction(undefined)\n        followBillingAction(result)\n      }\n    })\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-6\", className)}>\n      <div className=\"flex flex-col gap-1\">\n        <h2 className=\"text-base font-semibold\">{localization.billing}</h2>\n        <p className=\"text-sm text-muted-foreground\">\n          {localization.billingDescription}\n        </p>\n      </div>\n\n      <Card>\n        <CardHeader>\n          <div className=\"flex items-start justify-between gap-3\">\n            <div>\n              <CardTitle>{localization.subscription}</CardTitle>\n              <CardDescription>\n                {subscription\n                  ? (subscription.planName ?? subscription.planId)\n                  : localization.noSubscriptionDescription}\n              </CardDescription>\n            </div>\n            {subscription && (\n              <Badge variant=\"secondary\">{subscription.status}</Badge>\n            )}\n          </div>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          {state.isPending ? (\n            <div className=\"flex min-h-20 items-center justify-center\">\n              <Spinner />\n              <span className=\"sr-only\">{localization.loadingBilling}</span>\n            </div>\n          ) : subscription ? (\n            <>\n              {subscription.currentPeriodEnd && (\n                <p className=\"text-sm text-muted-foreground\">\n                  {(subscription.cancelAtPeriodEnd\n                    ? localization.endsOn\n                    : localization.renewsOn\n                  ).replace(\n                    \"{{date}}\",\n                    new Intl.DateTimeFormat(undefined, {\n                      dateStyle: \"medium\"\n                    }).format(subscription.currentPeriodEnd)\n                  )}\n                </p>\n              )}\n              {typeof subscription.seats === \"number\" &&\n                adapter.supports.seats && (\n                  <SeatsEditor\n                    key={subscription.id}\n                    seats={subscription.seats}\n                    isPending={updateSeats.isPending}\n                    onSave={(seats) =>\n                      updateSeats.mutate(\n                        { subscriptionId: subscription.id, seats },\n                        { onSuccess: followBillingAction }\n                      )\n                    }\n                  />\n                )}\n            </>\n          ) : (\n            <p className=\"text-sm text-muted-foreground\">\n              {localization.noSubscription}\n            </p>\n          )}\n        </CardContent>\n        <CardFooter className=\"flex-wrap gap-2\">\n          <Button\n            variant=\"outline\"\n            disabled={portal.isPending}\n            onClick={() =>\n              portal.mutate(undefined, { onSuccess: followBillingAction })\n            }\n          >\n            {portal.isPending ? <Spinner /> : <ExternalLink />}\n            {localization.manageBilling}\n          </Button>\n          {subscription?.cancelAtPeriodEnd && adapter.supports.restore ? (\n            <Button variant=\"outline\" onClick={() => setAction(\"restore\")}>\n              <RotateCcw />\n              {localization.restoreSubscription}\n            </Button>\n          ) : subscription && adapter.supports.cancel ? (\n            <Button variant=\"ghost\" onClick={() => setAction(\"cancel\")}>\n              <X />\n              {localization.cancelSubscription}\n            </Button>\n          ) : null}\n        </CardFooter>\n      </Card>\n\n      {state.data?.usage.length ? (\n        <Card>\n          <CardHeader>\n            <CardTitle>{localization.usage}</CardTitle>\n          </CardHeader>\n          <CardContent className=\"flex flex-col gap-4\">\n            {state.data.usage.map((usage) => (\n              <div key={usage.id} className=\"flex flex-col gap-2\">\n                <div className=\"flex items-center justify-between gap-3 text-sm\">\n                  <span className=\"font-medium\">{usage.label}</span>\n                  <span className=\"text-muted-foreground\">\n                    {usage.limit\n                      ? `${usage.used} / ${usage.limit}${usage.unit ? ` ${usage.unit}` : \"\"}`\n                      : localization.used.replace(\n                          \"{{used}}\",\n                          String(usage.used)\n                        )}\n                  </span>\n                </div>\n                <Progress\n                  value={\n                    usage.limit\n                      ? Math.min(100, (usage.used / usage.limit) * 100)\n                      : 0\n                  }\n                  aria-label={usage.label}\n                />\n              </div>\n            ))}\n          </CardContent>\n        </Card>\n      ) : null}\n\n      <section\n        className=\"flex flex-col gap-3\"\n        aria-labelledby=\"billing-plans-heading\"\n      >\n        <div className=\"flex items-end justify-between gap-3\">\n          <h3 id=\"billing-plans-heading\" className=\"text-sm font-semibold\">\n            {localization.plans}\n          </h3>\n          {intervals.length > 1 && (\n            <Select\n              value={resolvedInterval}\n              onValueChange={(value) => setInterval(value as BillingInterval)}\n            >\n              <SelectTrigger className=\"w-36\" aria-label={localization.plans}>\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                {intervals.map((entry) => (\n                  <SelectItem key={entry} value={entry}>\n                    {entry === \"month\"\n                      ? localization.perMonth\n                      : entry === \"year\"\n                        ? localization.perYear\n                        : localization.oneTime}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          )}\n        </div>\n        {plans.isPending ? (\n          <div className=\"flex min-h-40 items-center justify-center\">\n            <Spinner />\n            <span className=\"sr-only\">{localization.loadingBilling}</span>\n          </div>\n        ) : (\n          <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-3\">\n            {plans.data?.map((plan) => (\n              <PlanCard\n                key={plan.id}\n                plan={plan}\n                interval={resolvedInterval}\n                currentPlanId={subscription?.planId}\n                isPending={checkout.isPending}\n                onChoose={(selectedPlan, priceId) =>\n                  checkout.mutate(\n                    {\n                      planId: selectedPlan.id,\n                      priceId,\n                      seats: selectedPlan.seatBased ? 1 : undefined\n                    },\n                    { onSuccess: followBillingAction }\n                  )\n                }\n              />\n            ))}\n          </div>\n        )}\n      </section>\n\n      <AlertDialog\n        open={Boolean(action)}\n        onOpenChange={(open) => !open && setAction(undefined)}\n      >\n        <AlertDialogContent>\n          <AlertDialogHeader>\n            <AlertDialogMedia>\n              <CreditCard />\n            </AlertDialogMedia>\n            <AlertDialogTitle>\n              {action === \"cancel\"\n                ? localization.cancelSubscriptionTitle\n                : localization.restoreSubscriptionTitle}\n            </AlertDialogTitle>\n            <AlertDialogDescription>\n              {action === \"cancel\"\n                ? localization.cancelSubscriptionDescription\n                : localization.restoreSubscriptionDescription}\n            </AlertDialogDescription>\n          </AlertDialogHeader>\n          <AlertDialogFooter>\n            <AlertDialogCancel disabled={isActionPending}>\n              {localization.cancel}\n            </AlertDialogCancel>\n            <Button\n              type=\"button\"\n              variant={action === \"cancel\" ? \"destructive\" : \"default\"}\n              disabled={isActionPending}\n              onClick={handleAction}\n            >\n              {isActionPending && <Spinner />}\n              {localization.confirm}\n            </Button>\n          </AlertDialogFooter>\n        </AlertDialogContent>\n      </AlertDialog>\n    </div>\n  )\n}\n\nexport function UserBillingSettings(\n  props: Omit<BillingSettingsProps, \"adapter\" | \"scope\">\n) {\n  const { authClient } = useAuth()\n  const { data: session } = useSession(authClient)\n  const { adapter } = useAuthPlugin(billingPlugin)\n  if (!session?.user.id) return null\n  return (\n    <BillingSettings\n      {...props}\n      adapter={adapter}\n      scope={{ type: \"user\", userId: session.user.id }}\n    />\n  )\n}\n\nexport function OrganizationBillingSettings({\n  organizationId,\n  organizationSlug,\n  ...props\n}: Omit<BillingSettingsProps, \"adapter\" | \"scope\"> & {\n  organizationId: string\n  organizationSlug: string\n}) {\n  const { adapter } = useAuthPlugin(billingPlugin)\n  if (!organizationId || !organizationSlug) return null\n\n  return (\n    <BillingSettings\n      {...props}\n      adapter={adapter}\n      scope={{ type: \"organization\", organizationId, organizationSlug }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/auth/billing/billing-settings.tsx"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/auth-plugin.ts",
      "content": "import type {\n  AuthPluginComponents,\n  AuthPlugin as AuthPluginPrimitive\n} from \"@better-auth-ui/react\"\n\ndeclare module \"@better-auth-ui/core\" {\n  /** Widens `useAuth().plugins` to the shadcn-typed `AuthPlugin`. */\n  interface AuthPluginRegister {\n    shadcn: AuthPlugin\n  }\n}\n\n/** Props the shadcn `<Auth>` router spreads onto plugin-contributed auth views. */\nexport type AuthViewProps = {\n  className?: string\n  socialLayout?: \"auto\" | \"horizontal\" | \"vertical\" | \"grid\"\n  socialPosition?: \"top\" | \"bottom\"\n}\n\n/** Props the shadcn `<Settings>` router spreads onto plugin-contributed settings views. */\nexport type SettingsViewProps = {\n  className?: string\n}\n\n/** Shadcn plugin type. Plugin authors import this from `@/lib/auth/auth-plugin`. */\nexport type AuthPlugin = AuthPluginPrimitive<\n  AuthPluginComponents,\n  AuthViewProps,\n  SettingsViewProps\n>\n",
      "type": "registry:lib",
      "target": "@lib/auth/auth-plugin.ts"
    },
    {
      "path": "examples/start-shadcn-example/src/lib/auth/billing-plugin.ts",
      "content": "import { createAuthPlugin } from \"@better-auth-ui/core\"\nimport {\n  type BillingPluginOptions,\n  billingPlugin as coreBillingPlugin\n} from \"@better-auth-ui/core/plugins/billing\"\nimport { CreditCard } from \"lucide-react\"\nimport { createElement } from \"react\"\n\nimport {\n  OrganizationBillingSettings,\n  UserBillingSettings\n} from \"@/components/auth/billing/billing-settings\"\n\nconst billingLabel = (label: string) =>\n  createElement(\n    \"span\",\n    { className: \"flex items-center gap-1\" },\n    createElement(CreditCard),\n    label\n  )\n\nexport const billingPlugin = createAuthPlugin(\n  coreBillingPlugin.id,\n  (options: BillingPluginOptions) => {\n    const core = coreBillingPlugin(options)\n    return {\n      ...core,\n      ...(core.user\n        ? {\n            settingsTabs: [\n              {\n                view: \"billing\" as const,\n                label: billingLabel(core.localization.billing),\n                component: UserBillingSettings\n              }\n            ]\n          }\n        : {}),\n      ...(core.organization\n        ? {\n            organizationTabs: [\n              {\n                id: \"billing\",\n                path: core.viewPaths.settings.billing,\n                label: billingLabel(core.localization.billing),\n                component: OrganizationBillingSettings\n              }\n            ]\n          }\n        : {})\n    }\n  }\n)\n",
      "type": "registry:lib",
      "target": "@lib/auth/billing-plugin.ts"
    }
  ],
  "type": "registry:component"
}