@crediball/react 0.4.0 → 0.6.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.
@@ -27,16 +27,32 @@ export interface CrediballProviderProps {
27
27
  */
28
28
  amounts?: number[];
29
29
  /**
30
- * Called when the user picks an amount to top up (legacy flat-amount flow).
31
- * Wire this to your checkout or server-side top-up route. If omitted,
32
- * picking an amount just closes the modal.
30
+ * Optional override for the custom-amount top-up. If omitted, the paywall
31
+ * starts a Crediball-hosted Stripe Checkout itself (using publishableKey +
32
+ * userId) and redirects — no backend wiring needed, just connect payouts in the
33
+ * dashboard. Provide this only to run your own top-up/checkout flow instead.
33
34
  */
34
35
  onTopup?: (amount: number) => void | Promise<void>;
35
- /** Called when the user picks a one-time package. Call `meter.topup({ userId, packageId })` server-side. */
36
+ /**
37
+ * Optional override for one-time packages. If omitted, the paywall starts a
38
+ * Crediball-hosted Stripe Checkout itself and redirects (connect payouts in the
39
+ * dashboard). Provide this only to run your own flow (e.g. `meter.topup({ userId,
40
+ * packageId })` server-side).
41
+ */
36
42
  onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
37
- /** Called when the user picks a subscription plan. Call `meter.subscribe({ userId, planId })` server-side. */
43
+ /**
44
+ * Optional override for subscription plans. If omitted, the paywall starts a
45
+ * recurring Stripe Checkout itself (connect payouts in the dashboard) and Stripe
46
+ * grants the plan's credits each period. Provide this only to run your own flow
47
+ * (e.g. `meter.subscribe({ userId, planId })` server-side).
48
+ */
38
49
  onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
39
- /** Called when the user cancels their subscription. Call `meter.cancelSubscription({ userId, subscriptionId })` server-side. */
50
+ /**
51
+ * Called when the user cancels their subscription. Cancellation is server-side
52
+ * only (it needs your secret key), so the paywall shows a Cancel button on the
53
+ * active-subscription banner ONLY when this is provided — wire it to
54
+ * `meter.cancelSubscription({ userId, subscriptionId })` on your backend.
55
+ */
40
56
  onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
41
57
  /**
42
58
  * Auto-detect insufficient-credit responses from any fetch() call and open the
@@ -47,9 +63,15 @@ export interface CrediballProviderProps {
47
63
  * ndjson (streaming/SSE routes where the error arrives as a stream chunk).
48
64
  */
49
65
  watchFetch?: boolean;
50
- /** Allow a free-form amount entry in the paywall. */
66
+ /**
67
+ * Show a free-form amount field in the paywall. Defaults to your dashboard's
68
+ * custom-topup setting (with its min/max) when packages are loaded — set this
69
+ * explicitly only to override (e.g. `false` to force-hide it).
70
+ */
51
71
  allowCustom?: boolean;
72
+ /** Override the custom-amount minimum (defaults to the dashboard's configured minimum). */
52
73
  customMin?: number;
74
+ /** Override the custom-amount maximum (defaults to the dashboard's configured maximum). */
53
75
  customMax?: number;
54
76
  /** Currency symbol shown in the paywall (default "€"). Match your app's billing currency. */
55
77
  currencySymbol?: string;
@@ -38,7 +38,7 @@ const getEmptySnapshot = () => EMPTY_SNAPSHOT;
38
38
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
39
39
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
40
40
  */
41
- export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch = true, allowCustom = false, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
41
+ export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch = true, allowCustom, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
42
42
  const [open, setOpen] = useState(false);
43
43
  const showPaywall = useCallback(() => setOpen(true), []);
44
44
  const hidePaywall = useCallback(() => setOpen(false), []);
@@ -92,40 +92,115 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
92
92
  topUp,
93
93
  refresh,
94
94
  }), [showPaywall, hidePaywall, open, snapshot, costOf, topUp, refresh]);
95
+ // The page to return to after Stripe checkout — the one the user is on now.
96
+ function currentReturnPath() {
97
+ if (typeof window === "undefined")
98
+ return undefined;
99
+ return window.location.pathname + window.location.search;
100
+ }
101
+ // Drive a Crediball-hosted Stripe Checkout with just the publishable key — used
102
+ // when the host didn't wire its own onSelectPackage/onTopup handler, so the
103
+ // paywall's buttons work out of the box (the developer only has to connect
104
+ // payouts in the dashboard). Redirects the browser to Stripe on success.
105
+ async function startBuiltInCheckout(input) {
106
+ if (!client)
107
+ return;
108
+ const { url } = await client.createCheckout({ ...input, returnPath: currentReturnPath() });
109
+ if (typeof window !== "undefined")
110
+ window.location.assign(url);
111
+ }
95
112
  async function handleAdd(amount) {
96
- try {
97
- await topUp(amount);
113
+ // A host-supplied top-up flow always wins.
114
+ if (onTopup) {
115
+ try {
116
+ await topUp(amount);
117
+ }
118
+ finally {
119
+ setOpen(false);
120
+ }
121
+ return;
98
122
  }
99
- finally {
100
- setOpen(false);
123
+ // Otherwise start Stripe ourselves (custom amount). Leave the modal open on
124
+ // failure so the error is visible instead of the modal silently closing.
125
+ if (client) {
126
+ try {
127
+ await startBuiltInCheckout({ eurAmount: amount });
128
+ }
129
+ catch (err) {
130
+ console.error("[crediball] checkout failed:", err);
131
+ }
132
+ return;
101
133
  }
134
+ // Data-less mode with no handler: nothing to charge against.
135
+ setOpen(false);
102
136
  }
103
137
  async function handleSelectPackage(pkg) {
104
- try {
105
- await onSelectPackage?.(pkg);
106
- notifyBalanceChanged();
107
- await client?.refresh();
138
+ if (onSelectPackage) {
139
+ try {
140
+ await onSelectPackage(pkg);
141
+ notifyBalanceChanged();
142
+ await client?.refresh();
143
+ }
144
+ finally {
145
+ setOpen(false);
146
+ }
147
+ return;
108
148
  }
109
- finally {
110
- setOpen(false);
149
+ if (client) {
150
+ try {
151
+ await startBuiltInCheckout({ packageId: pkg.id });
152
+ }
153
+ catch (err) {
154
+ console.error("[crediball] checkout failed:", err);
155
+ }
156
+ return;
111
157
  }
158
+ setOpen(false);
112
159
  }
113
160
  async function handleSelectPlan(plan) {
114
- try {
115
- await onSelectPlan?.(plan);
116
- notifyBalanceChanged();
117
- await client?.refresh();
161
+ // Host-supplied subscription flow wins.
162
+ if (onSelectPlan) {
163
+ try {
164
+ await onSelectPlan(plan);
165
+ notifyBalanceChanged();
166
+ await client?.refresh();
167
+ }
168
+ finally {
169
+ setOpen(false);
170
+ }
171
+ return;
118
172
  }
119
- finally {
120
- setOpen(false);
173
+ // Otherwise start the recurring Stripe Checkout ourselves. Leave the modal
174
+ // open on failure so the error is visible.
175
+ if (client) {
176
+ try {
177
+ const { url } = await client.createSubscriptionCheckout({
178
+ planId: plan.id,
179
+ returnPath: currentReturnPath(),
180
+ });
181
+ if (typeof window !== "undefined")
182
+ window.location.assign(url);
183
+ }
184
+ catch (err) {
185
+ console.error("[crediball] subscription checkout failed:", err);
186
+ }
187
+ return;
121
188
  }
189
+ setOpen(false);
122
190
  }
123
191
  async function handleCancelSubscription(subscriptionId) {
124
192
  await onCancelSubscription?.(subscriptionId);
125
193
  await client?.refresh();
126
194
  }
127
195
  const pkgs = snapshot.packages;
128
- return (_jsxs(CrediballContext.Provider, { value: ctx, children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: handleCancelSubscription, amounts: amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: allowCustom, customMin: customMin, customMax: customMax, currencySymbol: currencySymbol, balance: snapshot.balance ?? undefined, balanceRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, currency: pkgs?.currency, usage: snapshot.usage?.usedCredits ?? undefined, title: title, description: description, accentColor: accentColor })] }));
196
+ // Auto-derive the custom-amount field from the dashboard's custom-topup config,
197
+ // so a developer who enabled it doesn't also have to pass allowCustom/min/max.
198
+ // Explicit props still win (allowCustom={false} force-hides it).
199
+ const customCfg = pkgs?.customTopup;
200
+ const effectiveAllowCustom = allowCustom ?? customCfg?.enabled ?? false;
201
+ const effectiveCustomMin = customMin ?? customCfg?.minEur;
202
+ const effectiveCustomMax = customMax ?? customCfg?.maxEur ?? undefined;
203
+ return (_jsxs(CrediballContext.Provider, { value: ctx, children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: handleCancelSubscription, amounts: amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: effectiveAllowCustom, customMin: effectiveCustomMin, customMax: effectiveCustomMax, currencySymbol: currencySymbol, balance: snapshot.balance ?? undefined, balanceRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, currency: pkgs?.currency, usage: snapshot.usage?.usedCredits ?? undefined, title: title, description: description, accentColor: accentColor })] }));
129
204
  }
130
205
  /**
131
206
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
@@ -130,7 +130,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
130
130
  borderRadius: theme.radiusCard,
131
131
  background: "#f5f5f7",
132
132
  border: `1px solid ${theme.hairline}`,
133
- }, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 600, color: theme.ink }, children: cancelDone ? "Subscription canceled" : `${activeSubscription.planName} · ${activeSubscription.planPeriod}` }), !cancelDone && (_jsx("button", { onClick: handleCancelSubscription, disabled: cancelPending, style: {
133
+ }, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 600, color: theme.ink }, children: cancelDone ? "Subscription canceled" : `${activeSubscription.planName} · ${activeSubscription.planPeriod}` }), onCancelSubscription && !cancelDone && (_jsx("button", { onClick: handleCancelSubscription, disabled: cancelPending, style: {
134
134
  marginTop: 8,
135
135
  appearance: "none",
136
136
  border: "none",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Drop-in React components for showing Crediball credits inside your AI app.",
5
5
  "license": "MIT",
6
6
  "author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
@@ -51,7 +51,7 @@
51
51
  "react": ">=18"
52
52
  },
53
53
  "dependencies": {
54
- "@crediball/core": "^0.1.0"
54
+ "@crediball/core": "^0.2.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",