@crediball/react 0.10.0 → 0.11.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.
@@ -54,6 +54,18 @@ export interface CrediballProviderProps {
54
54
  * `meter.cancelSubscription({ userId, subscriptionId })` on your backend.
55
55
  */
56
56
  onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
57
+ /**
58
+ * Optional override for the auto-topup opt-in. If omitted, the paywall starts a
59
+ * Crediball-hosted Stripe Checkout in setup mode itself (connect payouts in the
60
+ * dashboard) — no money changes hands there, it only saves a payment method.
61
+ * Provide this only to run your own flow (e.g.
62
+ * `meter.setupAutoTopupCheckout({ userId, packageId, thresholdCredits, ... })`
63
+ * server-side).
64
+ */
65
+ onEnableAutoTopup?: (input: {
66
+ packageId: string;
67
+ thresholdCredits: number;
68
+ }) => void | Promise<void>;
57
69
  /**
58
70
  * Auto-detect insufficient-credit responses from any fetch() call and open the
59
71
  * paywall automatically — no per-call wiring. Defaults to true. Set false to only
@@ -172,7 +184,7 @@ interface CrediballContextValue {
172
184
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
173
185
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
174
186
  */
175
- export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
187
+ export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
176
188
  /**
177
189
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
178
190
  *
@@ -63,7 +63,7 @@ function symbolForCurrency(iso) {
63
63
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
64
64
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
65
65
  */
66
- 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, applyRemoteTheme = true, captureReferrals = true, }) {
66
+ export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch = true, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme = true, captureReferrals = true, }) {
67
67
  const [open, setOpen] = useState(false);
68
68
  // Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
69
69
  // count the click) as soon as the provider mounts — the visitor usually has
@@ -240,6 +240,18 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
240
240
  await onCancelSubscription?.(subscriptionId);
241
241
  await client?.refresh();
242
242
  }
243
+ function handleEnableAutoTopup(input) {
244
+ return runPaywallAction(onEnableAutoTopup, input, async () => {
245
+ if (!client)
246
+ return;
247
+ const { url } = await client.createAutoTopupCheckout({
248
+ ...input,
249
+ returnPath: currentReturnPath(),
250
+ });
251
+ if (typeof window !== "undefined")
252
+ window.location.assign(url);
253
+ }, "auto-topup setup");
254
+ }
243
255
  // Auto-derive the custom-amount field from the dashboard's custom-topup config,
244
256
  // so a developer who enabled it doesn't also have to pass allowCustom/min/max.
245
257
  // Explicit props still win (allowCustom={false} force-hides it).
@@ -252,7 +264,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
252
264
  // layout box. Host props/CSS are overridden by design (dashboard wins).
253
265
  const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
254
266
  const hasThemeVars = Object.keys(themeVars).length > 0;
255
- const inner = (_jsxs(_Fragment, { children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: onCancelSubscription ? handleCancelSubscription : undefined, 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 ?? uiContent?.paywallTitle, description: description ?? uiContent?.paywallDescription, addButtonText: uiContent?.paywallButtonText, referralLink: referral?.link ?? undefined, referralReward: referral?.rewardCredits, accentColor: accentColor })] }));
267
+ const inner = (_jsxs(_Fragment, { children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: onCancelSubscription ? handleCancelSubscription : undefined, autoTopup: pkgs?.autoTopup, onEnableAutoTopup: handleEnableAutoTopup, 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 ?? uiContent?.paywallTitle, description: description ?? uiContent?.paywallDescription, addButtonText: uiContent?.paywallButtonText, referralLink: referral?.link ?? undefined, referralReward: referral?.rewardCredits, referralLabel: uiContent?.referralLabel, accentColor: accentColor })] }));
256
268
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
257
269
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
258
270
  }
@@ -22,6 +22,13 @@ export interface ActiveSubscriptionInfo {
22
22
  planPeriod: string;
23
23
  currentPeriodEnd: string;
24
24
  }
25
+ /** The end user's current auto-topup rule (returned by listPackages() with userId). */
26
+ export interface AutoTopupInfo {
27
+ enabled: boolean;
28
+ packageId: string;
29
+ thresholdCredits: number;
30
+ status: "pending" | "active" | "needs_auth" | "failed";
31
+ }
25
32
  export interface PaywallModalProps extends CrediballThemeOverrides {
26
33
  open: boolean;
27
34
  /**
@@ -58,6 +65,24 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
58
65
  * Call `meter.cancelSubscription({ userId, subscriptionId })` in your backend.
59
66
  */
60
67
  onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
68
+ /**
69
+ * The end user's current auto-topup rule (from `meter.listPackages({ userId })`
70
+ * or `meter.getAutoTopupStatus(userId)`). When absent/null and `onEnableAutoTopup`
71
+ * is provided, an opt-in row is shown below the packages so the user can turn
72
+ * on "keep me topped up automatically".
73
+ */
74
+ autoTopup?: AutoTopupInfo | null;
75
+ /**
76
+ * Called when the user opts into auto top-up (only shown when `packages` is
77
+ * non-empty). Call `meter.setupAutoTopupCheckout({ userId, packageId,
78
+ * thresholdCredits, successUrl, cancelUrl })` in your backend — it saves a
79
+ * payment method, no money changes hands in this step — then redirect the
80
+ * browser to the returned `url`.
81
+ */
82
+ onEnableAutoTopup?: (input: {
83
+ packageId: string;
84
+ thresholdCredits: number;
85
+ }) => void;
61
86
  /**
62
87
  * Legacy: flat list of euro amounts. Prefer `packages` + `onSelectPackage`.
63
88
  * When both are provided, `packages` takes precedence.
@@ -105,6 +130,12 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
105
130
  referralLink?: string | null;
106
131
  /** Credits earned per successful referral, shown in the refer-a-friend blurb. */
107
132
  referralReward?: number;
133
+ /**
134
+ * Label above the referral link. `{credits}` is replaced with `referralReward`.
135
+ * Default "Refer a friend · earn {credits} credits". Shown as written (not
136
+ * uppercased), so lowercase/sentence case is preserved.
137
+ */
138
+ referralLabel?: string;
108
139
  style?: CSSProperties;
109
140
  }
110
141
  /**
@@ -112,4 +143,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
112
143
  * continue. The host controls when it appears; shows a small "Powered by
113
144
  * Crediball" line by default (set `hideBranding` to remove it).
114
145
  */
115
- export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, addButtonText, customMin, customMax, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
146
+ export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, addButtonText, customMin, customMax, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
@@ -19,7 +19,7 @@ function sectionLabelStyle(marginBottom) {
19
19
  * continue. The host controls when it appears; shows a small "Powered by
20
20
  * Crediball" line by default (set `hideBranding` to remove it).
21
21
  */
22
- export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, amounts = [5, 10], amountPrefix = "Add €", title = "You need more credits to continue.", description = "Top up to keep using this feature.", onAdd, onClose, allowCustom = false, addButtonText = "Add", customMin = 1, customMax, currencySymbol = "€", balance, balanceRate, currency = "EUR", usage, usageLabel = "Usage", hideBranding = false, referralLink, referralReward, accentColor, style, }) {
22
+ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts = [5, 10], amountPrefix = "Add €", title = "You need more credits to continue.", description = "Top up to keep using this feature.", onAdd, onClose, allowCustom = false, addButtonText = "Add", customMin = 1, customMax, currencySymbol = "€", balance, balanceRate, currency = "EUR", usage, usageLabel = "Usage", hideBranding = false, referralLink, referralReward, referralLabel = "Refer a friend · earn {credits} credits", accentColor, style, }) {
23
23
  // Stack the top-up buttons one-per-line when the dialog itself is narrow.
24
24
  // Measured off the dialog's own box (via ResizeObserver) rather than the
25
25
  // window — in normal use the two are the same width, but this also makes
@@ -32,6 +32,9 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
32
32
  const [cancelPending, setCancelPending] = useState(false);
33
33
  const [cancelDone, setCancelDone] = useState(false);
34
34
  const [referralCopied, setReferralCopied] = useState(false);
35
+ const [autoTopupOpen, setAutoTopupOpen] = useState(false);
36
+ const [autoTopupPackageId, setAutoTopupPackageId] = useState("");
37
+ const [autoTopupThreshold, setAutoTopupThreshold] = useState("");
35
38
  useEffect(() => {
36
39
  const el = overlayRef.current;
37
40
  if (!el || typeof ResizeObserver === "undefined")
@@ -43,6 +46,15 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
43
46
  observer.observe(el);
44
47
  return () => observer.disconnect();
45
48
  }, []);
49
+ // Default the auto-topup picker to the cheapest package + its own credit
50
+ // amount as the suggested threshold, once packages are available.
51
+ useEffect(() => {
52
+ if (!packages || packages.length === 0 || autoTopupPackageId)
53
+ return;
54
+ const cheapest = packages.reduce((min, p) => (p.price < min.price ? p : min), packages[0]);
55
+ setAutoTopupPackageId(cheapest.id);
56
+ setAutoTopupThreshold(String(cheapest.credits));
57
+ }, [packages, autoTopupPackageId]);
46
58
  if (!open)
47
59
  return null;
48
60
  async function handleCancelSubscription() {
@@ -81,6 +93,12 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
81
93
  setCustomError(null);
82
94
  onAdd?.(amt);
83
95
  }
96
+ function submitAutoTopup() {
97
+ const threshold = Number(autoTopupThreshold);
98
+ if (!autoTopupPackageId || !Number.isFinite(threshold) || threshold < 0)
99
+ return;
100
+ onEnableAutoTopup?.({ packageId: autoTopupPackageId, thresholdCredits: threshold });
101
+ }
84
102
  const buttonStyle = {
85
103
  flex: 1,
86
104
  appearance: "none",
@@ -95,6 +113,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
95
113
  padding: "8px 16px",
96
114
  borderRadius: theme.radiusPill,
97
115
  };
116
+ const resolvedReferralLabel = referralLabel.replace("{credits}", formatCredits(referralReward ?? 0));
98
117
  return (_jsx("div", { ref: overlayRef, role: "dialog", "aria-modal": "true", style: {
99
118
  position: "fixed",
100
119
  inset: 0,
@@ -170,13 +189,73 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
170
189
  color: theme.ink,
171
190
  background: theme.canvas,
172
191
  outline: "none",
173
- } }), _jsx("button", { onClick: submitCustom, style: { ...buttonStyle, flex: "none" }, children: addButtonText })] }), customError ? (_jsx("p", { style: { margin: "8px 0 0", fontSize: 13, color: theme.accent }, children: customError })) : null] })) : null, referralLink ? (_jsxs("div", { style: {
174
- marginTop: 20,
192
+ } }), _jsx("button", { onClick: submitCustom, style: { ...buttonStyle, flex: "none" }, children: addButtonText })] }), customError ? (_jsx("p", { style: { margin: "8px 0 0", fontSize: 13, color: theme.accent }, children: customError })) : null] })) : null, onEnableAutoTopup && packages && packages.length > 0 && (_jsx("div", { style: { marginTop: 16 }, children: autoTopup?.enabled && autoTopup.status === "active" ? (_jsxs("p", { style: { margin: 0, fontSize: 12, color: theme.inkMuted }, children: ["Auto top-up is on \u2014 buying", " ", packages.find((p) => p.id === autoTopup.packageId)?.name ?? "your package", " whenever your balance drops below ", formatCredits(autoTopup.thresholdCredits), " credits."] })) : autoTopup?.enabled && (autoTopup.status === "needs_auth" || autoTopup.status === "failed") ? (_jsxs("div", { children: [_jsx("p", { style: { margin: 0, fontSize: 12, color: theme.accent }, children: autoTopup.status === "needs_auth"
193
+ ? "Auto top-up needs you to re-confirm your payment method."
194
+ : "Auto top-up's last charge failed." }), _jsx("button", { onClick: () => onEnableAutoTopup({
195
+ packageId: autoTopup.packageId,
196
+ thresholdCredits: autoTopup.thresholdCredits,
197
+ }), style: {
198
+ marginTop: 6,
199
+ appearance: "none",
200
+ border: "none",
201
+ background: "transparent",
202
+ cursor: "pointer",
203
+ padding: 0,
204
+ fontSize: 12,
205
+ color: theme.ink,
206
+ fontFamily: theme.font,
207
+ textDecoration: "underline",
208
+ }, children: "Fix auto top-up" })] })) : autoTopupOpen ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("select", { value: autoTopupPackageId, onChange: (e) => setAutoTopupPackageId(e.target.value), style: {
209
+ flex: 1,
210
+ minWidth: 0,
211
+ fontFamily: theme.font,
212
+ fontSize: 13,
213
+ padding: "6px 10px",
214
+ borderRadius: theme.radiusPill,
215
+ border: `1px solid ${theme.hairline}`,
216
+ color: theme.ink,
217
+ background: theme.canvas,
218
+ }, children: packages.map((pkg) => (_jsxs("option", { value: pkg.id, children: [pkg.name, " \u00B7 ", formatMoney(pkg.price, currency)] }, pkg.id))) }), _jsx("input", { value: autoTopupThreshold, onChange: (e) => setAutoTopupThreshold(e.target.value), inputMode: "numeric", "aria-label": "Threshold credits", style: {
219
+ width: 72,
220
+ fontFamily: theme.font,
221
+ fontSize: 13,
222
+ padding: "6px 10px",
223
+ borderRadius: theme.radiusPill,
224
+ border: `1px solid ${theme.hairline}`,
225
+ color: theme.ink,
226
+ background: theme.canvas,
227
+ } }), _jsx("button", { onClick: submitAutoTopup, style: { ...buttonStyle, flex: "none", padding: "6px 14px", fontSize: 13 }, children: "Turn on" })] }), _jsx("p", { style: { margin: 0, fontSize: 11, color: theme.inkMuted }, children: "Buy that package automatically whenever your balance drops below the credit amount above." })] })) : (_jsx("button", { onClick: () => setAutoTopupOpen(true), style: {
228
+ appearance: "none",
229
+ border: "none",
230
+ background: "transparent",
231
+ cursor: "pointer",
232
+ padding: 0,
233
+ fontSize: 12,
234
+ color: theme.inkMuted,
235
+ fontFamily: theme.font,
236
+ textDecoration: "underline",
237
+ }, children: "Keep me topped up automatically" })) })), onClose ? (_jsx("button", { onClick: onClose, style: {
238
+ width: "100%",
239
+ marginTop: 12,
240
+ appearance: "none",
241
+ border: "none",
242
+ cursor: "pointer",
243
+ background: "transparent",
244
+ color: theme.inkMuted,
245
+ fontFamily: theme.font,
246
+ fontSize: 12,
247
+ padding: "8px 16px",
248
+ borderRadius: theme.radiusPill,
249
+ }, children: "Not now" })) : null, referralLink ? (_jsxs("div", { style: {
250
+ marginTop: 16,
175
251
  paddingTop: 16,
176
252
  borderTop: `1px solid ${theme.hairline}`,
177
- }, children: [_jsx("div", { style: sectionLabelStyle(8), children: referralReward && referralReward > 0
178
- ? `Refer a friend · earn ${formatCredits(referralReward)} credits`
179
- : "Refer a friend" }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { title: referralLink, style: {
253
+ }, children: [_jsx("div", { style: {
254
+ fontSize: 12,
255
+ fontWeight: 600,
256
+ color: theme.inkMuted,
257
+ marginBottom: 8,
258
+ }, children: resolvedReferralLabel }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { title: referralLink, style: {
180
259
  flex: 1,
181
260
  minWidth: 0,
182
261
  overflow: "hidden",
@@ -200,17 +279,5 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
200
279
  padding: "8px 16px",
201
280
  borderRadius: theme.radiusPill,
202
281
  whiteSpace: "nowrap",
203
- }, children: referralCopied ? "Copied!" : "Copy" })] })] })) : null, onClose ? (_jsx("button", { onClick: onClose, style: {
204
- width: "100%",
205
- marginTop: 12,
206
- appearance: "none",
207
- border: "none",
208
- cursor: "pointer",
209
- background: "transparent",
210
- color: theme.inkMuted,
211
- fontFamily: theme.font,
212
- fontSize: 12,
213
- padding: "8px 16px",
214
- borderRadius: theme.radiusPill,
215
- }, children: "Not now" })) : null, !hideBranding && (_jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 10 }, children: _jsx("span", { style: { fontSize: 10, color: theme.inkMuted }, children: "Powered by Crediball." }) }))] }) }));
282
+ }, children: referralCopied ? "Copied!" : "Copy" })] })] })) : null, !hideBranding && (_jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 10 }, children: _jsx("span", { style: { fontSize: 10, color: theme.inkMuted }, children: "Powered by Crediball." }) }))] }) }));
216
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.10.0",
3
+ "version": "0.11.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.4.0"
54
+ "@crediball/core": "^0.6.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",