@nozle-js/react 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @nozle-js/react
2
2
 
3
- Browser-safe React components for Nozle's public plan catalog and merchant-controlled checkout.
3
+ React components for Nozle's public catalog, checkout, and customer billing portal.
4
4
 
5
5
  ## Credential boundary
6
6
 
7
7
  - `pk_` is used only for `GET /api/v1/plans`.
8
8
  - Secret keys stay on the merchant backend.
9
- - The React package does not fetch customer billing, invoice, subscription, entitlement, or credit data.
10
- - The React package never sends a customer ID.
9
+ - Presentational components consume caller-supplied customer state.
10
+ - `BillingPortal` uses a customer-scoped session from your backend for Core GraphQL reads.
11
+ - Your backend chooses the customer and authorizes the selected external subscription ID.
11
12
 
12
13
  ## Install
13
14
 
14
15
  ```bash
15
- npm install @nozle-js/react react react-dom @stripe/stripe-js @stripe/react-stripe-js
16
+ npm install @nozle-js/react@0.10.0 react react-dom @stripe/stripe-js @stripe/react-stripe-js
16
17
  ```
17
18
 
18
19
  ## Merchant-backed checkout
@@ -47,13 +48,114 @@ export function BillingPage({ csrfToken }: { csrfToken: string }) {
47
48
 
48
49
  The merchant endpoint authenticates the user, derives the Nozle customer from the authenticated user or team, validates the plan and HTTPS return URL, and calls Nozle with a restricted server-side `sk_`. Any browser-supplied customer identifier must be ignored or rejected.
49
50
 
50
- `createCheckout` may return a hosted URL, an embedded Stripe client secret, `type: "completed"`, or `type: "scheduled"`. Paid plans activate only after Nozle processes verified Stripe success; a browser redirect is not proof of payment.
51
+ `createCheckout` returns a `stripe`, `razorpay`, `hosted`, `processing`, `completed`, or `scheduled` result. Nozle payment fulfillment determines activation; Razorpay requires capture. A browser redirect or callback alone does not prove activation.
51
52
 
52
53
  ## Components
53
54
 
54
55
  - `PricingTable` and `usePlans` read the public catalog with the publishable key.
55
56
  - `CheckoutButton`, `UpgradeButton`, `UpgradeModal`, and the default `PricingTable` CTA call the merchant's `createCheckout` callback.
56
- - `Checkout` renders Stripe hosted/embedded checkout results supplied by the merchant.
57
+ - `Checkout` renders provider checkout results and accepts merchant verification/status callbacks directly.
58
+ - `BillingPortal`, `CancellationControl`, and `PlanChangeControl` provide customer subscription management through authenticated merchant adapters.
57
59
  - Gates, usage displays, `PlanBadge`, and `PaymentMethodDisplay` are presentational and consume caller-supplied data.
58
60
 
59
- Customer billing, invoices, cancellation, top-ups, subscriptions, entitlements, and credits belong behind authenticated merchant endpoints. Never put an `sk_`, master key, or internal credential in browser code.
61
+ Authenticate portal sessions and management actions on your merchant backend. Return customer-scoped portal tokens at runtime; never embed an `sk_`, master key, or portal token in a static browser bundle.
62
+
63
+ ## Native billing portal and subscription management
64
+
65
+ `BillingPortal` renders subscriptions, current usage, invoices/PDFs, billing details, and
66
+ wallets directly in React. It does not require `BillingProvider` or Tailwind. Reads use a
67
+ customer-scoped Core session; optional `cancellationActions` enable Cancel + Keep through
68
+ your authenticated merchant backend and the Node.js or Python SDK. Optional `planChangeActions`
69
+ add quoted upgrades, scheduled downgrades, exact pending-change withdrawal, and payment recovery.
70
+
71
+ ```tsx
72
+ import {
73
+ BillingPortal, BillingPortalError,
74
+ type CancellationActions, type CreateBillingPortalSession,
75
+ } from "@nozle-js/react";
76
+
77
+ async function post(path: string, body: object, signal: AbortSignal) {
78
+ const response = await fetch(path, {
79
+ method: "POST", credentials: "same-origin",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify(body), signal,
82
+ });
83
+ if (!response.ok) throw new BillingPortalError(
84
+ [401, 403].includes(response.status) ? "unauthorized" :
85
+ response.status === 409 ? "changed" : "request",
86
+ );
87
+ return response.json();
88
+ }
89
+ const createSession: CreateBillingPortalSession = ({ signal }) =>
90
+ post("/api/billing/session", {}, signal);
91
+ const cancellationActions: CancellationActions = {
92
+ preview: ({ signal, ...input }) => post("/api/billing/cancellation/preview", input, signal),
93
+ apply: ({ signal, ...input }) => post("/api/billing/cancellation", input, signal),
94
+ };
95
+ export function BillingPage({ accountId }: { accountId: string }) {
96
+ return <BillingPortal key={accountId} createSession={createSession}
97
+ cancellationActions={cancellationActions} locale="en-US" />;
98
+ }
99
+ ```
100
+
101
+ The matching runnable Node merchant example and React app are in
102
+ [`examples/billing-portal`](../../examples/billing-portal). The Python SDK repository has the
103
+ equivalent `examples/billing_portal` backend. They authenticate a server-selected demo customer
104
+ and require same-origin JSON requests. For your app, derive the customer from the authenticated
105
+ user/team, retain your CSRF protection, reject browser-supplied customer identities, and check
106
+ subscription ownership on the server.
107
+
108
+ ### Session and action contracts
109
+
110
+ - `createSession({ signal })` returns `{ token, apiUrl? }`. On your server, call Core's
111
+ `GET /api/v1/customers/{external_id}/portal_url` and extract the token from `/customer-portal/{token}`.
112
+ Return it with the Core base URL (default `https://api.nozle.app/core`). The component sends
113
+ `customer-portal-token` on GraphQL requests. Self-hosted Core must allow your app origin/header.
114
+ - `preview({ subscriptionId, operation, signal })` returns `{ operation, effectiveAt, renewalAt }`.
115
+ The identifier is the **external subscription ID**. Operations are `cancel` and `uncancel`.
116
+ Dates are ISO strings; `renewalAt` may be null. Preserve the exact `effectiveAt` string.
117
+ - `apply({ subscriptionId, operation, idempotencyKey, expectedEffectiveAt?, signal })` uses the
118
+ existing server SDK transition method. On the server, choose `timing: "end_of_period"` for
119
+ cancel and forward `expectedEffectiveAt` as the `expected_effective_at` guard. Core rejects a
120
+ changed date before mutation. Uncancel accepts no settlement overrides. Retain merchant-side
121
+ idempotency records across restarts so uncertain retries use the same upstream key.
122
+ - Map HTTP409 state/date conflicts to `BillingPortalError("changed")`, requiring a new preview.
123
+ Map expired authentication to `"unauthorized"`. Never display raw upstream error bodies.
124
+
125
+ Without `cancellationActions`, the overview remains usable and cancel/keep controls are absent.
126
+ After each attempt the portal reads the selected subscription through customer-scoped GraphQL.
127
+ Cancellation stays separate from active status; Keep is available before the ending boundary.
128
+ Cancellation removes any pending plan change. Keep restores renewal without restoring the
129
+ removed change. Outstanding invoices and usage charges remain subject to merchant policy.
130
+
131
+ Use the exported `CancellationControl`, `CancellationSubscription`, and `CancellationActions`
132
+ on a custom billing page. Supply `refresh(signal)` to read persisted subscription state from
133
+ your authenticated source. It must not return an optimistic local update. The component
134
+ supports `onChanged`, redacted `onError`, `locale`, `timezone`, and stylesheet `nonce`.
135
+
136
+ Keep `createSession` stable and change its identity or the portal's `key` when the authenticated
137
+ customer changes. Remount a standalone cancellation control on customer changes too. Old
138
+ requests are aborted and previous customer content is hidden; portal tokens stay in memory.
139
+
140
+ Customize with `className`, `style`, `nonce`, and CSS variables `--nozle-portal-accent`,
141
+ `--nozle-portal-background`, `--nozle-portal-text`, `--nozle-portal-muted`,
142
+ `--nozle-portal-muted-background`, and `--nozle-portal-border`. UI copy is English; `locale`
143
+ formats amounts/dates. Errors use `code: "session" | "unauthorized" | "request" | "changed"`.
144
+
145
+ ### Plan changes and payment recovery
146
+
147
+ Pass `planChangeActions` to `BillingPortal`, or use the standalone `PlanChangeControl`.
148
+ Its adapter provides `load`, `status`, `preview`, `apply`, and `withdraw`; use
149
+ `verifyCheckout` and `getCheckoutStatus` for scoped payment verification/recovery.
150
+ The Node.js `0.9.0` and Python `0.9.0` SDKs provide the matching backend methods.
151
+
152
+ Load eligible plans for the selected external subscription, confirm the signed quote,
153
+ and retain the same quote and idempotency key for uncertain retries. Upgrades activate
154
+ after required payment is fulfilled; downgrades use end-of-period timing with `keep_anchor`.
155
+ Withdrawal targets the exact internal pending UUID. Reload authoritative subscription
156
+ and checkout state after submission or a page reload.
157
+
158
+ See the [billing portal guide](https://docs.nozle.dev/sdks/react/billing-portal) for
159
+ complete typed adapters, standalone controls, and the runnable Node/Python integrations.
160
+ The hosted Nozle backend supports these flows; self-hosted deployments need the matching
161
+ subscription-management endpoints and cancellation-date guard.