跳到主要内容
知仓学习社ZHICANG

billing-sdk

Guide for building billing UI with BillingSDK - the open-source React component library for pricing tables, subscription management, usage meters, i…

不碰外部(只输出文字)严重 2 · 高危 0hashgraph-online/awesome-codex-plugins

它会碰到什么

扫了多少1 个文本文件,18 KB
它会碰到什么不碰外部(只输出文字)
命中总数4 处
命中统计严重 2 · 高 0 · 中 0 · 低 0
逐条看命中(2 条严重或高危)
  • 严重 SKILL.md:55instruction-harmful-additive
    npx @billingsdk/cli add pricing-table-one
  • 严重 SKILL.md:61instruction-harmful-additive
    npx shadcn@latest add @billingsdk/pricing-table-one

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

BillingSDK

BillingSDK is an open-source React component library for billing interfaces, maintained by Dodo Payments

at github.com/dodopayments/billingsdk. Components are copied into your repository as source files

(shadcn/ui model) rather than imported from a runtime npm package.

Use this skill when building the front end. All Dodo Payments API calls belong in server-side route

handlers; the components are presentation only.

When to use this skill

  • Building a pricing page that starts a Dodo checkout.
  • Building an account/billing page with subscription details and a "Manage billing" button.
  • Rendering usage meters, credit balances, invoice history, or upcoming charges.
  • Adding cancel / upgrade / downgrade UI to an existing React or Next.js app.
  • Deciding whether to scaffold with @billingsdk/cli init or add individual components.

Core concepts

Source-installed, not a dependency. The repository root package billingsdk is private. There is no

verified general-purpose runtime npm component package. You install components through the CLI or the

shadcn registry, and the .tsx files land in your project under components/billingsdk/.

The installed file is the prop contract. Because components are vendored into your repo, the exported

props type in components/billingsdk/<component>.tsx is authoritative for your version. Read it before

wiring callbacks. Do not assume prop names from another project.

The registry is the installability source of truth. Use

registry.json. The docs navigation

has drifted from it — for example payment-success-dialog appears in navigation but not in the current

registry. If add fails, the block is not in the registry.

Components never call Dodo. They receive data and fire callbacks. Every Dodo Payments call runs on your

server with DODO_PAYMENTS_API_KEY. That key is dodo_test_... or dodo_live_... and must never reach

the browser.

Installation

Scaffold a new integration (framework config, API routes, useBilling hooks, lib/dodopayments.ts,

dependencies, and env vars DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_ENVIRONMENT,

DODO_PAYMENTS_WEBHOOK_KEY):

npx @billingsdk/cli init

Add a single component to an existing project:

npx @billingsdk/cli add pricing-table-one

Or install through the shadcn registry using the @billingsdk/ namespace:

npx shadcn@latest add @billingsdk/pricing-table-one

The published CLI package is @billingsdk/cli, most recently seen at version 0.9.0.

Component inventory

From the current official registry, grouped by purpose:

| Group | Blocks |

|---|---|

| Pricing | pricing-table-one through pricing-table-eight |

| Subscription | subscription-management, usage-based-pricing, invoice-history, update-plan-card, update-plan-dialog, proration-preview, cancel-subscription-card, cancel-subscription-dialog |

| Usage and billing | usage-meter-linear, usage-meter-circle, usage-table, detailed-usage-table, billing-screen, billing-settings, billing-settings-2, upcoming-charges |

| Payments | payment-details, payment-details-two, payment-method-selector, payment-card, payment-failure |

| Promotion and trials | banner, limited-offer-dialog, trial-expiry-card |

pricing-table-one is the only block with an officially published prop example, reproduced below. For

every other block, run add and read the generated file's props type.

Server client

One module, imported only by route handlers and server components.

// lib/dodopayments.ts
import DodoPayments from 'dodopayments';

if (!process.env.DODO_PAYMENTS_API_KEY) {
  throw new Error('DODO_PAYMENTS_API_KEY is not set');
}

export const dodo = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: process.env.DODO_PAYMENTS_ENVIRONMENT === 'live_mode' ? 'live_mode' : 'test_mode',
});

environment defaults to 'live_mode' when omitted, so set it explicitly during development. Base URLs

are https://test.dodopayments.com and https://live.dodopayments.com.

Pricing page

1. Plan catalogue

Keep the product ids and the display copy in one module. The price here is a display number rendered by

the component — the amount actually charged comes from the Dodo product catalogue, and Dodo API amounts

are always in the smallest currency unit (cents). Treat this file as a mirror of the dashboard and

reconcile it when you change a price.

// lib/plans.ts
export type PlanId = 'starter' | 'pro';

export interface Plan {
  id: PlanId;
  title: string;
  price: number;
  period: string;
  features: string[];
  popular: boolean;
}

export const PLANS: readonly Plan[] = [
  {
    id: 'starter',
    title: 'Starter',
    price: 9,
    period: 'month',
    features: ['100 requests', 'Basic support', '1 project'],
    popular: false,
  },
  {
    id: 'pro',
    title: 'Pro',
    price: 29,
    period: 'month',
    features: ['Unlimited requests', 'Priority support', '10 projects'],
    popular: true,
  },
];

2. Product id map — server only

Never let the browser choose an arbitrary product_id. Map the public plan slug to a pdt_ id on the

server and reject anything else.

// lib/plan-products.server.ts
import 'server-only';
import type { PlanId } from './plans';

const PRODUCT_IDS: Record<PlanId, string> = {
  starter: 'pdt_starter_monthly',
  pro: 'pdt_pro_monthly',
};

export function productIdForPlan(value: unknown): string | null {
  if (typeof value !== 'string') return null;
  return PRODUCT_IDS[value as PlanId] ?? null;
}

3. Checkout route

// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { dodo } from '@/lib/dodopayments';
import { productIdForPlan } from '@/lib/plan-products.server';
import { getCurrentUser } from '@/lib/auth';

export async function POST(request: Request) {
  const user = await getCurrentUser();
  if (!user) {
    return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
  }

  const body: unknown = await request.json();
  const planId = (body as { planId?: unknown }).planId;
  const productId = productIdForPlan(planId);

  if (!productId) {
    return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
  }

  try {
    const session = await dodo.checkoutSessions.create({
      product_cart: [{ product_id: productId, quantity: 1 }],
      customer: { email: user.email, name: user.name },
      return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
      metadata: { app_user_id: user.id },
    });

    if (!session.checkout_url) {
      return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
    }

    return NextResponse.json({ checkoutUrl: session.checkout_url });
  } catch (error) {
    console.error('Checkout session creation failed', error);
    return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
  }
}

checkout_url is nullable — it is absent when the session is created with payment_method_id — so guard

it rather than redirecting to undefined. Checkout URLs are single-use and normally expire after 24 hours.

Use checkoutSessions.create. payments.create and subscriptions.create are deprecated for new

integrations. Checkout parameters are covered in depth in the checkout-integration skill.

4. Client component

// components/pricing-plans.tsx
'use client';

import { useState } from 'react';
import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
import { PLANS } from '@/lib/plans';

export function PricingPlans() {
  const [error, setError] = useState<string | null>(null);

  async function handlePlanSelect(planId: string) {
    setError(null);
    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ planId }),
      });

      if (!response.ok) {
        setError('Could not start checkout. Please try again.');
        return;
      }

      const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
      window.location.href = checkoutUrl;
    } catch {
      setError('Network error. Please try again.');
    }
  }

  return (
    <>
      <PricingTableOne
        plans={PLANS}
        title="Choose your plan"
        description="Select the plan that works best for you"
        onPlanSelect={handlePlanSelect}
        theme="classic"
        size="medium"
      />
      {error ? <p role="alert">{error}</p> : null}
    </>
  );
}

Render <PricingPlans /> from app/pricing/page.tsx. Nothing secret crosses into the client component:

it sends a plan slug and receives a URL.

Subscription management and customer portal

Install the block, then read its props from the generated file:

npx @billingsdk/cli add subscription-management

The portal route. The method is customers.customerPortal.create(customerID, { ...params }) and the

session's URL field is link.

// app/api/portal/route.ts
import { NextResponse } from 'next/server';
import { dodo } from '@/lib/dodopayments';
import { getCurrentUser } from '@/lib/auth';

export async function POST() {
  const user = await getCurrentUser();
  if (!user?.dodoCustomerId) {
    return NextResponse.json({ error: 'No billing account' }, { status: 400 });
  }

  try {
    const session = await dodo.customers.customerPortal.create(user.dodoCustomerId, {
      return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
    });

    return NextResponse.json({ url: session.link });
  } catch (error) {
    console.error('Customer portal session failed', error);
    return NextResponse.json({ error: 'Could not open billing portal' }, { status: 500 });
  }
}

Portal links expire after 24 hours, so mint one per click instead of caching it. In the portal a customer

can view and cancel subscriptions, change plans within enabled product collections, update payment

methods, download invoices, and retrieve license keys.

Client side:

// components/manage-billing-button.tsx
'use client';

import { useState } from 'react';

export function ManageBillingButton() {
  const [pending, setPending] = useState(false);

  async function openPortal() {
    setPending(true);
    try {
      const response = await fetch('/api/portal', { method: 'POST' });
      if (!response.ok) {
        setPending(false);
        return;
      }
      const { url } = (await response.json()) as { url: string };
      window.location.href = url;
    } catch {
      setPending(false);
    }
  }

  return (
    <button type="button" onClick={openPortal} disabled={pending}>
      {pending ? 'Opening…' : 'Manage billing'}
    </button>
  );
}

Wire openPortal to whichever callback prop your installed subscription-management.tsx exposes, or

render the standalone button next to it. Load the subscription itself in the server component that hosts

the page — await dodo.subscriptions.retrieve(user.dodoSubscriptionId) — and pass plain data down.

Usage and credit display

Install a meter block:

npx @billingsdk/cli add usage-meter-linear

Fetch the numbers server-side. Credit balances come from creditEntitlements.balances.retrieve:

// app/api/usage/credits/route.ts
import { NextResponse } from 'next/server';
import { dodo } from '@/lib/dodopayments';
import { getCurrentUser } from '@/lib/auth';

const CREDIT_ENTITLEMENT_ID = process.env.DODO_CREDIT_ENTITLEMENT_ID;

export async function GET() {
  const user = await getCurrentUser();
  if (!user?.dodoCustomerId || !CREDIT_ENTITLEMENT_ID) {
    return NextResponse.json({ error: 'No billing account' }, { status: 400 });
  }

  try {
    const balance = await dodo.creditEntitlements.balances.retrieve(user.dodoCustomerId, {
      credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
    });

    return NextResponse.json({ balance: balance.balance, overage: balance.overage });
  } catch (error) {
    console.error('Credit balance lookup failed', error);
    return NextResponse.json({ error: 'Could not read balance' }, { status: 500 });
  }
}

Pass the result into the meter block using the prop names in your installed

components/billingsdk/usage-meter-linear.tsx. Related blocks for the same surface: usage-meter-circle,

usage-table, detailed-usage-table, upcoming-charges, invoice-history.

Credit balances are eventually consistent — meter-to-credit deduction runs on a background worker roughly

once a minute — so the displayed balance is informational. Do not gate a request on it. Metering, credit

entitlements, and ledger entries are covered in the usage-based-billing and credit-based-billing skills.

Theming and customization

Components are Tailwind CSS plus shadcn/ui conventions, so there are three layers:

  1. Theme tokens. Override the shadcn CSS variables in globals.css; every block inherits them.
   @layer base {
     :root {
       --primary: 220 90% 56%;
       --primary-foreground: 0 0% 100%;
     }
   }
  1. Component props. pricing-table-one accepts theme ("classic" in the official example) and

size ("medium"). Check the installed file for the full union types.

  1. Source edits. The files are yours. Editing them is the supported path for structural changes, at

the cost of manual reconciliation when you re-run add.

The hosted Dodo checkout page is themed separately, through customization.theme_config on the checkout

session — not by these components.

Framework support

The components are not framework-agnostic. They require React or Next.js plus Tailwind CSS and

shadcn/ui conventions.

The CLI additionally ships server integration templates for Next.js, Express, Hono, Fastify, and React.

That covers the route-handler side only. Caution: Dodo's integration page and BillingSDK's own

introduction have disagreed about which server templates are shipping versus "coming soon" — confirm the

adapter you want with the current CLI before committing to it.

For non-React front ends, call Dodo through a framework adapter (@dodopayments/nextjs,

@dodopayments/express, @dodopayments/sveltekit, and others) and build your own UI.

Environment variables

# .env.local — server-side only, never prefixed with NEXT_PUBLIC_
DODO_PAYMENTS_API_KEY=dodo_test_xxxxxxxxxxxxxxxx
DODO_PAYMENTS_ENVIRONMENT=test_mode
DODO_PAYMENTS_WEBHOOK_KEY=xxxxxxxxxxxxxxxx
DODO_CREDIT_ENTITLEMENT_ID=cde_xxxxxxxxxxxxxxxx

# Safe to expose
NEXT_PUBLIC_APP_URL=https://yoursite.com

Switch to dodo_live_... and DODO_PAYMENTS_ENVIRONMENT=live_mode together. A live key against

test_mode fails, and a test key against live_mode fails.

Common mistakes

Exposing the API key to the browser. NEXT_PUBLIC_DODO_PAYMENTS_API_KEY inlines the secret into the

JS bundle for anyone to read. There is no publishable key in Dodo Payments — every key is secret. Import

lib/dodopayments.ts only from route handlers and server components.

Calling customers.createPortalSession. That method does not exist. Use

client.customers.customerPortal.create(customerID, { ...params }) and read .link, not .url.

Trusting client-rendered plan state. The selected plan in React state, or the fact that the user

returned to return_url, does not mean the subscription is active. Grant entitlements only from

webhook-confirmed subscription events persisted in your database, then render UI from that. Webhook

signature verification is covered in the webhook-integration skill.

Accepting a raw product_id from the request body. A caller can then check out against any product in

your catalogue, including internal or discounted ones. Map an opaque plan slug to a pdt_ id server-side.

Hardcoding prices in the client. PLANS[].price is display text. When you change a price in the Dodo

dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.

Reconcile lib/plans.ts with the dashboard as part of any pricing change.

npm install billingsdk. The root repository package is private. Install through

@billingsdk/cli or the shadcn registry instead.

Guessing prop names. pricing-table-one is the only block with a published prop example. Open the

generated file under components/billingsdk/ and read the exported props type before wiring anything else.

Using DODO_PAYMENTS_WEBHOOK_SECRET. The SDK reads DODO_PAYMENTS_WEBHOOK_KEY for the webhookKey

option.

Gating requests on the displayed credit balance. Deduction is asynchronous, so the reported balance

lags. Dodo explicitly warns against using it as strict per-request authorization.

Using payments.create or subscriptions.create for a purchase. Both are deprecated. Use

checkoutSessions.create.

Resources

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。