@djangocfg/payments

中文文档
2.1.655发布于 2026/11/12MITTypeScriptESM + CJS近 30 天下载 1.4万

Provider-agnostic React payments module (Stripe-first): checkout state machine, Payment Element wrapper, close-guard context, mock adapter for keyless development

原始使用文档
文档目录9 节
  1. The seam
  2. Layering
  3. Styles (Tailwind v4) — required wiring
  4. Public API
  5. Conventions
  6. Card on file
  7. Configuration & keys
  8. Stripe setup (test mode)
  9. Going live

@djangocfg/payments

Provider-agnostic React payments module. Stripe-first, but the package never depends on a concrete SDK — the host injects an adapter. Battle-tested in production (one-time PaymentIntent checkout, recurring subscriptions with Stripe Link, card on file) before extraction into @djangocfg/*.

📖 Full documentation → — architecture, the server contract, Stripe traps, testing.

The seam

The host picks one adapter and injects it via <PaymentProvider>. Package hooks/components read it through usePaymentAdapter() and never import Stripe or a generated API client directly.

import { PaymentProvider, createMockPaymentAdapter } from '@djangocfg/payments';

// Mock-first: build & verify the whole UX before any backend exists.
<PaymentProvider adapter={createMockPaymentAdapter()}>
  {children}
</PaymentProvider>

Swapping to real Stripe is a one-line host change:

// loadStripe is re-exported by this package — the host never declares @stripe/*.
import { PaymentProvider, createStripeAdapter, loadStripe } from '@djangocfg/payments';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PK!); // once, module scope
const adapter = createStripeAdapter({
  getStripe: () => stripePromise,
  createIntentOnBackend: (input) => api.payments.createIntent(input), // host transport
});

<PaymentProvider adapter={adapter}>{children}</PaymentProvider>;

Layering

domain/      pure types + money/card helpers   (no React, no Stripe)
context/     <PaymentProvider> + <CheckoutGuard>   (the seam, the in-flight lock)
transport/   mock-adapter | stripe-adapter      (the SDK seam; see the note below)
hooks/       useCheckout, useCardSetup, usePaymentHistory, useSavedCards
components/  CheckoutForm, CardSetupForm, StripePaymentElement, dialogs, tables, badges

@stripe/* is a runtime dependency of exactly two files (transport/stripe-adapter.ts, components/StripePaymentElement.tsx) and a type-only one of a third (components/appearance.ts) — enforced by scripts/check-stripe-imports.mjs, which pnpm check runs. @stripe/stripe-js is only the tiny CDN loader (the real SDK loads from js.stripe.com at runtime), so tree-shaking drops the Stripe components from mock-only bundles. See @docs/ARCHITECTURE.md for why this is one package and where the boundaries are.

Styles (Tailwind v4) — required wiring

This package is consumed from source and styled with Tailwind utility classes. Tailwind v4 only generates classes it can see, so the consumer's global CSS must import the package's styles entry (it carries the @source directive that points Tailwind at this package's sources):

/* your app's globals.css */
@import "@djangocfg/ui-core/styles/full";
@import "@djangocfg/payments/styles";   /* ← add this */

Symptom when missing: components render unstyled in spots — e.g. PaymentStatusBadge chips lose their background tint and show as plain text. Next.js hosts also add the package to transpilePackages.

Public API

ExportKindPurpose
PaymentProvider / usePaymentAdaptercontextinject / read the adapter
createMockPaymentAdapteradapterfull fake state machine (success / declined / 3DS / cards)
createStripeAdapteradapterreal Stripe (host supplies key + backend calls)
useCheckouthookidle → creating → requires_payment → processing → succeeded|failed|requires_action
useCardSetuphookthe same machine for saving a card with no charge
useSavedCardshookcards on file as a phase (unavailable|loading|ready|error) + set-default / remove
usePaymentHistoryhookpaginated history via adapter.listPayments
CheckoutDialogcomponentclose-locked dialog shell; wrap your checkout body in it
CheckoutFormcomponentprovider-agnostic form shell; inject the field via paymentField
CardSetupFormcomponentthe same, for saving a card (no amount, no "Paid")
UpdateCardDialogcomponentthe whole add-a-card flow: intent on open, confirm, report
SavedCardListcomponentcards table: default badge, expiry warning, row actions
StripePaymentElementcomponent<Elements> + <PaymentElement>; render-props the live Elements
PaymentHistorycomponentsortable table of PaymentRecord
PaymentStatusBadgecomponentstatus pill on ui-core semantic tokens
toMinorUnits / toMajorUnits / formatAmountutilmoney at the boundary (integer minor units)
cardExpiresAt / isCardExpired / isCardExpiringSoon / formatCardExpiry / sortCards / cardsOfutilcard derivations against a clock

Conventions

  • Source-consumed (no bundler); main/types/exports → ./src/index.ts.
  • ui-core flat import. No widget dependency: a package may not depend on a widget (widgets/CLAUDE.md).
  • Does not mount its own <UiProviders> — the host provides it once.
  • Locale-free: user-facing labels are props/slots (host supplies i18n).
  • Verify with pnpm -F @djangocfg/payments check and … lint.

Card on file

A subscription outlives the card that pays for it: when a renewal fails Stripe retries for ~2 weeks against the same stored card, then gives up. Without a way to replace it, a customer who wanted to pay loses the subscription.

const { phase, canAddCard, canManageCards, refresh, setDefault, remove, isBusy } =
  useSavedCards();

<SavedCardList
  phase={phase}
  busy={isBusy}
  onAddCard={canAddCard ? () => setOpen(true) : undefined}
  onSetDefault={canManageCards ? setDefault : undefined}
  onRemove={canManageCards ? remove : undefined}
/>
<UpdateCardDialog open={open} onClose={close} onSaved={() => { close(); void refresh(); }} />

Four host endpoints, wired all-or-none, plus four server rules that fail silently — see @docs/CARD_ON_FILE.md and @docs/SERVER_CONTRACT.md.

Not Link, not the Billing Portal. Link is a buyer-side wallet — link.com does not know your subscription exists, and a renewal charges the stored pm_…. The Billing Portal is a server-side redirect that discards this package's theming and the host's locale. Reasoning in @docs/STRIPE_NOTES.md.

Configuration & keys

Three Stripe keys, split by where they're safe to live. Stripe ships test (sk_test_…, pk_test_…) and live (sk_live_…, pk_live_…) flavours with the same variable names; only the values differ. Bring up test first.

KeyPrefixLives inExposed to browser?
Secret keysk_test_… / sk_live_…Backend only (STRIPE__SECRET_KEY)❌ never
Webhook signing secretwhsec_…Backend only (STRIPE__WEBHOOK_SECRET)❌ never
Publishable keypk_test_… / pk_live_…Backend + Frontend✅ safe (public by design)
# backend, git-ignored secrets file
STRIPE__SECRET_KEY=sk_test_xxx
STRIPE__PUBLISHABLE_KEY=pk_test_xxx
STRIPE__WEBHOOK_SECRET=whsec_xxx          # comma-separate to rotate: whsec_new,whsec_old

# your Next.js app .env
NEXT_PUBLIC_STRIPE_PK=pk_test_xxx

With no NEXT_PUBLIC_STRIPE_PK set, the host falls back to the mock adapter — the whole UX still renders (no real charge).

⚠️ The secret (sk_) and webhook (whsec_) keys must live in a git-ignored file or a secret manager. Only pk_… is safe in tracked config.

Stripe setup (test mode)

  1. Test keys — Dashboard → Developers → API keys (toggle "test mode").
  2. Webhook — locally, the Stripe CLI prints a whsec_… for the forwarded endpoint:
    stripe listen --forward-to localhost:8000/<your-webhook-path>/
    
    Deployed: create the endpoint in the Dashboard and subscribe to payment_intent.succeeded, payment_intent.payment_failed, charge.refunded (plus the invoice/subscription events your dunning needs).
  3. Test a payment — card 4242 4242 4242 4242, any future expiry/CVC.
    stripe trigger payment_intent.succeeded
    stripe events resend <evt_id>   # test idempotency / replay
    

Going live

  1. Switch all three STRIPE__* values and NEXT_PUBLIC_STRIPE_PK to live.
  2. Create a live webhook endpoint (live mode has its own signing secret).
  3. Register your domain for Apple Pay / Google Pay if you enable wallets.
  4. Do a small real charge to confirm end-to-end.

版本与发布记录

@djangocfg/payments 共发布 165 个版本,首次发布于 2026/07/10;过去 12 个月发布了 165 个版本。

版本发布日期距今
2.1.655latest2026/11/12今天
2.1.6532026/11/12今天
2.1.6522026/11/11今天
2.1.6512026/11/111 天前
2.1.6502026/11/111 天前
2.1.6482026/11/111 天前

关于 @djangocfg/payments 的常见问题

@djangocfg/payments 是什么?

Provider-agnostic React payments module (Stripe-first): checkout state machine, Payment Element wrapper, close-guard context, mock adapter for keyless development

如何安装 @djangocfg/payments?

在项目目录运行 npm install @djangocfg/payments,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.0.0)、react-dom(^19.0.0)、lucide-react(^1.68.0)、@djangocfg/ui-core(^2.1.655)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@djangocfg/payments 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@djangocfg/payments 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@djangocfg/payments 可以商用吗?

@djangocfg/payments 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@djangocfg/payments 的下载量有多少?

根据 npm 官方下载统计,@djangocfg/payments 在 2026/10/13 – 2026/11/11 这 30 天内被下载 13,594 次,日均约 453 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@djangocfg/payments 还在维护吗?

最新版本 2.1.655 发布于 2026/11/12(今天);至今共发布 165 个版本,其中过去 12 个月发布了 165 个;首次发布于 2026/07/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。