@crediball/react 0.5.0 → 0.7.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.
@@ -41,13 +41,18 @@ export interface CrediballProviderProps {
41
41
  */
42
42
  onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
43
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.
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
48
  */
49
49
  onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
50
- /** 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
+ */
51
56
  onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
52
57
  /**
53
58
  * Auto-detect insufficient-credit responses from any fetch() call and open the
@@ -68,7 +73,12 @@ export interface CrediballProviderProps {
68
73
  customMin?: number;
69
74
  /** Override the custom-amount maximum (defaults to the dashboard's configured maximum). */
70
75
  customMax?: number;
71
- /** Currency symbol shown in the paywall (default "€"). Match your app's billing currency. */
76
+ /**
77
+ * Currency symbol shown in the paywall. Auto-derived (via Intl) from the ISO
78
+ * currency you configured on the dashboard's Packages page once packages load,
79
+ * so it never drifts out of sync if you switch currencies there. Pass this only
80
+ * to force a specific symbol instead. Falls back to "€" before the first fetch.
81
+ */
72
82
  currencySymbol?: string;
73
83
  title?: string;
74
84
  description?: string;
@@ -127,7 +137,7 @@ interface CrediballContextValue {
127
137
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
128
138
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
129
139
  */
130
- export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
140
+ export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
131
141
  /**
132
142
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
133
143
  *
@@ -18,6 +18,22 @@ const EMPTY_SNAPSHOT = {
18
18
  };
19
19
  const noopSubscribe = () => () => { };
20
20
  const getEmptySnapshot = () => EMPTY_SNAPSHOT;
21
+ /**
22
+ * Map an ISO 4217 code (as returned by the dashboard's packages config) to a
23
+ * display symbol via Intl, so the paywall doesn't need a hand-maintained
24
+ * currency → symbol table that can fall out of sync with the dashboard.
25
+ */
26
+ function symbolForCurrency(iso) {
27
+ try {
28
+ const part = new Intl.NumberFormat(undefined, { style: "currency", currency: iso })
29
+ .formatToParts(0)
30
+ .find((p) => p.type === "currency");
31
+ return part?.value ?? iso;
32
+ }
33
+ catch {
34
+ return iso;
35
+ }
36
+ }
21
37
  /**
22
38
  * Pattern B, automated. Wrap your app once:
23
39
  *
@@ -38,7 +54,7 @@ const getEmptySnapshot = () => EMPTY_SNAPSHOT;
38
54
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
39
55
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
40
56
  */
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, }) {
57
+ export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch = true, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, }) {
42
58
  const [open, setOpen] = useState(false);
43
59
  const showPaywall = useCallback(() => setOpen(true), []);
44
60
  const hidePaywall = useCallback(() => setOpen(false), []);
@@ -158,20 +174,46 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
158
174
  setOpen(false);
159
175
  }
160
176
  async function handleSelectPlan(plan) {
161
- try {
162
- await onSelectPlan?.(plan);
163
- notifyBalanceChanged();
164
- await client?.refresh();
177
+ // Host-supplied subscription flow wins.
178
+ if (onSelectPlan) {
179
+ try {
180
+ await onSelectPlan(plan);
181
+ notifyBalanceChanged();
182
+ await client?.refresh();
183
+ }
184
+ finally {
185
+ setOpen(false);
186
+ }
187
+ return;
165
188
  }
166
- finally {
167
- setOpen(false);
189
+ // Otherwise start the recurring Stripe Checkout ourselves. Leave the modal
190
+ // open on failure so the error is visible.
191
+ if (client) {
192
+ try {
193
+ const { url } = await client.createSubscriptionCheckout({
194
+ planId: plan.id,
195
+ returnPath: currentReturnPath(),
196
+ });
197
+ if (typeof window !== "undefined")
198
+ window.location.assign(url);
199
+ }
200
+ catch (err) {
201
+ console.error("[crediball] subscription checkout failed:", err);
202
+ }
203
+ return;
168
204
  }
205
+ setOpen(false);
169
206
  }
170
207
  async function handleCancelSubscription(subscriptionId) {
171
208
  await onCancelSubscription?.(subscriptionId);
172
209
  await client?.refresh();
173
210
  }
174
211
  const pkgs = snapshot.packages;
212
+ // Follow the dashboard's configured currency once packages load, unless the
213
+ // host pins an explicit symbol. Falls back to "€" only until the first fetch
214
+ // resolves, so switching currencies on the Packages page never requires a
215
+ // matching code change/redeploy here.
216
+ const currencySymbol = currencySymbolProp ?? (pkgs?.currency ? symbolForCurrency(pkgs.currency) : "€");
175
217
  // Auto-derive the custom-amount field from the dashboard's custom-topup config,
176
218
  // so a developer who enabled it doesn't also have to pass allowCustom/min/max.
177
219
  // Explicit props still win (allowCustom={false} force-hides it).
@@ -179,11 +221,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
179
221
  const effectiveAllowCustom = allowCustom ?? customCfg?.enabled ?? false;
180
222
  const effectiveCustomMin = customMin ?? customCfg?.minEur;
181
223
  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 })] }));
224
+ 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 })] }));
187
225
  }
188
226
  /**
189
227
  * 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,10 +1,10 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.5.0",
3
+ "version": "0.7.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>",
7
- "homepage": "https://crediball.app",
7
+ "homepage": "https://crediball.ai",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/filipporezzadore/Crediball.git",
@@ -51,7 +51,7 @@
51
51
  "react": ">=18"
52
52
  },
53
53
  "dependencies": {
54
- "@crediball/core": "^0.1.1"
54
+ "@crediball/core": "^0.2.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",