@djangocfg/payments
v2.1.468
Published
Provider-agnostic React payments module (Stripe-first): checkout state machine, Payment Element wrapper, close-guard context, mock adapter for keyless development
Downloads
734
Maintainers
Readme
@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) before extraction into @djangocfg/*.
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 checkout 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 helpers (no React, no Stripe)
context/ <PaymentProvider> + usePaymentAdapter() (the injected seam)
transport/ mock-adapter | stripe-adapter (only stripe-adapter touches @stripe/*)
hooks/ useCheckout, usePaymentHistory (state machine over the adapter)
components/ CheckoutForm, StripePaymentElement, PaymentHistory, PaymentStatusBadge@stripe/* is touched in exactly two files (transport/stripe-adapter.ts,
components/StripePaymentElement.tsx) and ships as a regular dependency —
@stripe/stripe-js is only the tiny CDN loader (the real SDK loads from
js.stripe.com at runtime), and tree-shaking drops the Stripe components from
mock-only bundles.
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/ui-tools/styles";
@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
| Export | Kind | Purpose |
|---|---|---|
| PaymentProvider / usePaymentAdapter | context | inject / read the adapter |
| createMockPaymentAdapter | adapter | full fake state machine (success / declined / 3DS) |
| createStripeAdapter | adapter | real Stripe (host supplies key + create-intent call) |
| useCheckout | hook | idle → creating → requires_payment → processing → succeeded\|failed\|requires_action |
| usePaymentHistory | hook | paginated history via adapter.listPayments |
| CheckoutDialog | component | close-locked dialog shell (CheckoutGuard + ui-core Dialog); wrap your checkout body in it |
| CheckoutForm | component | provider-agnostic form shell (ui-core only); inject the provider field via paymentField |
| StripePaymentElement | component | <Elements> + <PaymentElement>; render-props the live Elements to the host |
| PaymentHistory | component | DataTable of PaymentRecord |
| PaymentStatusBadge | component | status pill on ui-core semantic tokens (success/warning/info/destructive) |
| toMinorUnits / toMajorUnits / formatAmount | util | money at the boundary (amounts cross as integer minor units) |
Conventions
- Source-consumed (no bundler);
main/types/exports→./src/index.ts. - ui-core flat import; ui-tools subpath-only (
@djangocfg/ui-tools/data-table). - 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(tsc).
Configuration & keys
Payments need three Stripe keys, split by where they're safe to live. Stripe
ships them in two flavours — test (sk_test_…, pk_test_…) and live
(sk_live_…, pk_live_…) — with the same variable names; only the values
differ. Bring up test first, verify end-to-end, then switch to live.
| Key | Prefix | Lives in | Exposed to browser? |
|---|---|---|---|
| Secret key | sk_test_… / sk_live_… | Backend only (STRIPE__SECRET_KEY) | ❌ never |
| Webhook signing secret | whsec_… | Backend only (STRIPE__WEBHOOK_SECRET) | ❌ never |
| Publishable key | pk_test_… / pk_live_… | Backend + Frontend | ✅ safe (it's public by design) |
The publishable key is typically returned to the browser by the backend's create-checkout response (
publishable_key) and/or read fromNEXT_PUBLIC_STRIPE_PK. The secret and webhook keys never leave the server.
Backend (Django) — STRIPE__*
This package is frontend-only; it pairs with any Django backend that creates
PaymentIntents (a built-in django_cfg.apps.payments module is planned — see
@plans/plan-payments-v1). Paths below are the reference host's; adjust env
names to your backend. Set the keys server-side (git-ignored secrets file):
# --- Stripe (test) ---
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
# --- Stripe (live) — same names, live values ---
# STRIPE__SECRET_KEY=sk_live_xxx
# STRIPE__PUBLISHABLE_KEY=pk_live_xxx
# STRIPE__WEBHOOK_SECRET=whsec_live_xxxFrontend — NEXT_PUBLIC_STRIPE_PK
# your Next.js app .env (test → pk_test_…, live → pk_live_…)
NEXT_PUBLIC_STRIPE_PK=pk_test_xxxWith no NEXT_PUBLIC_STRIPE_PK set, the host falls back to the mock
adapter — the whole checkout UX still renders (no real charge), which is the
mock-first dev path.
⚠️ Security: the secret (
sk_) and webhook (whsec_) keys must live in a git-ignored file or a secret manager — never commit them. If the project's.env*files are tracked by git, add the secrets file to.gitignoreandgit rm --cachedit first. Onlypk_…(publishable) is safe in tracked config.
Stripe setup (test mode)
- Get test keys — Stripe Dashboard → Developers → API keys (toggle "test
mode"): copy
pk_test_…andsk_test_…. - Webhook — for local dev, use the Stripe CLI (it prints a
whsec_…signing secret for the forwarded endpoint):
For a deployed env, create the endpoint in Dashboard → Developers → Webhooks pointing at your backend's webhook URL and subscribe to:stripe login stripe listen --forward-to localhost:8000/<your-webhook-path>/ # → copy the "whsec_…" it prints into STRIPE__WEBHOOK_SECRETpayment_intent.succeeded,payment_intent.payment_failed,charge.refunded. - Test a payment — open an order, hit Pay, use a Stripe test card
(
4242 4242 4242 4242, any future expiry/CVC). Trigger events directly with:stripe trigger payment_intent.succeeded stripe events resend <evt_id> # test idempotency / replay
Going live
- Switch all three
STRIPE__*values to their…_live_…counterparts andNEXT_PUBLIC_STRIPE_PKtopk_live_…. - Create a live webhook endpoint in the Dashboard (live mode has its own
signing secret — set it in
STRIPE__WEBHOOK_SECRET). - Register your domain for Apple Pay / Google Pay if you enable wallets.
- Do a small real charge to confirm end-to-end.
