@crediball/react 0.4.0 → 0.5.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,14 +27,25 @@ 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
+ * Called when the user picks a subscription plan. Unlike one-time top-ups,
45
+ * subscriptions have NO built-in checkout (they need recurring billing your
46
+ * backend sets up), so the paywall only shows subscription plans when this is
47
+ * provided — wire it to `meter.subscribe({ userId, planId })` server-side.
48
+ */
38
49
  onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
39
50
  /** Called when the user cancels their subscription. Call `meter.cancelSubscription({ userId, subscriptionId })` server-side. */
40
51
  onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
@@ -47,9 +58,15 @@ export interface CrediballProviderProps {
47
58
  * ndjson (streaming/SSE routes where the error arrives as a stream chunk).
48
59
  */
49
60
  watchFetch?: boolean;
50
- /** Allow a free-form amount entry in the paywall. */
61
+ /**
62
+ * Show a free-form amount field in the paywall. Defaults to your dashboard's
63
+ * custom-topup setting (with its min/max) when packages are loaded — set this
64
+ * explicitly only to override (e.g. `false` to force-hide it).
65
+ */
51
66
  allowCustom?: boolean;
67
+ /** Override the custom-amount minimum (defaults to the dashboard's configured minimum). */
52
68
  customMin?: number;
69
+ /** Override the custom-amount maximum (defaults to the dashboard's configured maximum). */
53
70
  customMax?: number;
54
71
  /** Currency symbol shown in the paywall (default "€"). Match your app's billing currency. */
55
72
  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,23 +92,70 @@ 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
161
  try {
@@ -125,7 +172,18 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
125
172
  await client?.refresh();
126
173
  }
127
174
  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 })] }));
175
+ // Auto-derive the custom-amount field from the dashboard's custom-topup config,
176
+ // so a developer who enabled it doesn't also have to pass allowCustom/min/max.
177
+ // Explicit props still win (allowCustom={false} force-hides it).
178
+ const customCfg = pkgs?.customTopup;
179
+ const effectiveAllowCustom = allowCustom ?? customCfg?.enabled ?? false;
180
+ const effectiveCustomMin = customMin ?? customCfg?.minEur;
181
+ const effectiveCustomMax = customMax ?? customCfg?.maxEur ?? undefined;
182
+ return (_jsxs(CrediballContext.Provider, { value: ctx, children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage,
183
+ // Subscriptions can't be fulfilled by the built-in checkout (that path is
184
+ // one-time top-ups only), so only OFFER them when the host wired an
185
+ // onSelectPlan to fulfill them — otherwise they'd be dead buttons.
186
+ subscriptionPlans: onSelectPlan ? pkgs?.subscriptionPlans : undefined, 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
187
  }
130
188
  /**
131
189
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.1.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",