@crediball/react 0.7.0 → 0.8.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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type PackageOption, type SubscriptionPlanOption } from "./PaywallModal";
|
|
3
|
-
import { type UsagePeriod } from "@crediball/core";
|
|
3
|
+
import { type UsagePeriod, type PackagesPayload, type CrediballUiContent } from "@crediball/core";
|
|
4
4
|
export interface CrediballProviderProps {
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
/**
|
|
@@ -84,6 +84,15 @@ export interface CrediballProviderProps {
|
|
|
84
84
|
description?: string;
|
|
85
85
|
/** Override the accent color (defaults to Action Blue). */
|
|
86
86
|
accentColor?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Apply the theme (colors, radii, font) the developer published on the
|
|
89
|
+
* Crediball dashboard's UI-patterns page — served alongside packages — as
|
|
90
|
+
* `--crediball-*` CSS variables, so preview edits reflect here with no code
|
|
91
|
+
* change. On by default; the published theme takes precedence over any
|
|
92
|
+
* `--crediball-*` vars you set in your own CSS. Set false to opt out and
|
|
93
|
+
* theme entirely from your own stylesheet.
|
|
94
|
+
*/
|
|
95
|
+
applyRemoteTheme?: boolean;
|
|
87
96
|
}
|
|
88
97
|
interface CrediballContextValue {
|
|
89
98
|
/**
|
|
@@ -116,6 +125,12 @@ interface CrediballContextValue {
|
|
|
116
125
|
topUp: (amount: number) => Promise<void>;
|
|
117
126
|
/** Refetch balance, usage, costs, and packages immediately. No-op without publishableKey/userId. */
|
|
118
127
|
refresh: () => Promise<void>;
|
|
128
|
+
/** The live packages payload (top-up options, plans, custom-topup, currency, ui). null until loaded. */
|
|
129
|
+
packages: PackagesPayload | null;
|
|
130
|
+
/** Currency symbol derived from the dashboard's configured currency (e.g. "€", "$"). */
|
|
131
|
+
currencySymbol: string;
|
|
132
|
+
/** Developer-published default copy from the dashboard, used as a label fallback. null when unconfigured. */
|
|
133
|
+
content: CrediballUiContent | null;
|
|
119
134
|
}
|
|
120
135
|
/**
|
|
121
136
|
* Pattern B, automated. Wrap your app once:
|
|
@@ -137,7 +152,7 @@ interface CrediballContextValue {
|
|
|
137
152
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
138
153
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
139
154
|
*/
|
|
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;
|
|
155
|
+
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, }: CrediballProviderProps): import("react").JSX.Element;
|
|
141
156
|
/**
|
|
142
157
|
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
143
158
|
*
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import { PaywallModal } from "./PaywallModal";
|
|
5
5
|
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
6
6
|
import { subscribeInsufficientCredits } from "./creditEvent";
|
|
7
|
-
import { getCrediballClient, notifyBalanceChanged } from "@crediball/core";
|
|
7
|
+
import { getCrediballClient, notifyBalanceChanged, } from "@crediball/core";
|
|
8
8
|
const CrediballContext = createContext(null);
|
|
9
9
|
const EMPTY_SNAPSHOT = {
|
|
10
10
|
balance: null,
|
|
@@ -34,6 +34,50 @@ function symbolForCurrency(iso) {
|
|
|
34
34
|
return iso;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Turn a published theme into the `--crediball-*` inline style the provider
|
|
39
|
+
* spreads onto a wrapping element. Only set fields are emitted, so an empty or
|
|
40
|
+
* partial theme leaves the built-in defaults (and any host CSS vars) in place.
|
|
41
|
+
*/
|
|
42
|
+
function themeToCssVars(theme) {
|
|
43
|
+
if (!theme)
|
|
44
|
+
return {};
|
|
45
|
+
const vars = {};
|
|
46
|
+
if (theme.accent)
|
|
47
|
+
vars["--crediball-accent"] = theme.accent;
|
|
48
|
+
if (theme.onAccent)
|
|
49
|
+
vars["--crediball-on-accent"] = theme.onAccent;
|
|
50
|
+
if (theme.ink)
|
|
51
|
+
vars["--crediball-ink"] = theme.ink;
|
|
52
|
+
if (theme.canvas)
|
|
53
|
+
vars["--crediball-canvas"] = theme.canvas;
|
|
54
|
+
if (theme.hairline)
|
|
55
|
+
vars["--crediball-hairline"] = theme.hairline;
|
|
56
|
+
if (theme.radius != null)
|
|
57
|
+
vars["--crediball-radius"] = `${theme.radius}px`;
|
|
58
|
+
if (theme.radiusCard != null)
|
|
59
|
+
vars["--crediball-radius-card"] = `${theme.radiusCard}px`;
|
|
60
|
+
if (theme.fontStack)
|
|
61
|
+
vars["--crediball-font"] = theme.fontStack;
|
|
62
|
+
return vars;
|
|
63
|
+
}
|
|
64
|
+
/** Inject a webfont stylesheet <link> once (idempotent by href), for a published theme's fontUrl. */
|
|
65
|
+
function loadRemoteFont(url) {
|
|
66
|
+
if (typeof document === "undefined")
|
|
67
|
+
return;
|
|
68
|
+
const existing = document.querySelector(`link[data-crediball-font]`);
|
|
69
|
+
if (existing?.href === url)
|
|
70
|
+
return;
|
|
71
|
+
if (existing)
|
|
72
|
+
existing.href = url;
|
|
73
|
+
else {
|
|
74
|
+
const link = document.createElement("link");
|
|
75
|
+
link.rel = "stylesheet";
|
|
76
|
+
link.href = url;
|
|
77
|
+
link.setAttribute("data-crediball-font", "");
|
|
78
|
+
document.head.appendChild(link);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
37
81
|
/**
|
|
38
82
|
* Pattern B, automated. Wrap your app once:
|
|
39
83
|
*
|
|
@@ -54,7 +98,7 @@ function symbolForCurrency(iso) {
|
|
|
54
98
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
55
99
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
56
100
|
*/
|
|
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, }) {
|
|
101
|
+
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, }) {
|
|
58
102
|
const [open, setOpen] = useState(false);
|
|
59
103
|
const showPaywall = useCallback(() => setOpen(true), []);
|
|
60
104
|
const hidePaywall = useCallback(() => setOpen(false), []);
|
|
@@ -84,6 +128,20 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
84
128
|
return () => client.stop();
|
|
85
129
|
}, [client]);
|
|
86
130
|
const snapshot = useSyncExternalStore(client ? client.subscribe : noopSubscribe, client ? client.getSnapshot : getEmptySnapshot, client ? client.getSnapshot : getEmptySnapshot);
|
|
131
|
+
const pkgs = snapshot.packages;
|
|
132
|
+
const uiTheme = pkgs?.ui?.theme ?? null;
|
|
133
|
+
const uiContent = pkgs?.ui?.content ?? null;
|
|
134
|
+
// Follow the dashboard's configured currency once packages load, unless the
|
|
135
|
+
// host pins an explicit symbol. Falls back to "€" only until the first fetch
|
|
136
|
+
// resolves, so switching currencies on the Packages page never requires a
|
|
137
|
+
// matching code change/redeploy here.
|
|
138
|
+
const currencySymbol = currencySymbolProp ?? (pkgs?.currency ? symbolForCurrency(pkgs.currency) : "€");
|
|
139
|
+
// Load the published theme's webfont, if any, so --crediball-font renders.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!applyRemoteTheme || !uiTheme?.fontUrl)
|
|
142
|
+
return;
|
|
143
|
+
loadRemoteFont(uiTheme.fontUrl);
|
|
144
|
+
}, [applyRemoteTheme, uiTheme?.fontUrl]);
|
|
87
145
|
const costOf = useCallback((event) => client?.costOf(event), [client]);
|
|
88
146
|
const refresh = useCallback(async () => {
|
|
89
147
|
await client?.refresh();
|
|
@@ -107,7 +165,10 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
107
165
|
costOf,
|
|
108
166
|
topUp,
|
|
109
167
|
refresh,
|
|
110
|
-
|
|
168
|
+
packages: pkgs,
|
|
169
|
+
currencySymbol,
|
|
170
|
+
content: uiContent,
|
|
171
|
+
}), [showPaywall, hidePaywall, open, snapshot, costOf, topUp, refresh, pkgs, currencySymbol, uiContent]);
|
|
111
172
|
// The page to return to after Stripe checkout — the one the user is on now.
|
|
112
173
|
function currentReturnPath() {
|
|
113
174
|
if (typeof window === "undefined")
|
|
@@ -208,12 +269,6 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
208
269
|
await onCancelSubscription?.(subscriptionId);
|
|
209
270
|
await client?.refresh();
|
|
210
271
|
}
|
|
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) : "€");
|
|
217
272
|
// Auto-derive the custom-amount field from the dashboard's custom-topup config,
|
|
218
273
|
// so a developer who enabled it doesn't also have to pass allowCustom/min/max.
|
|
219
274
|
// Explicit props still win (allowCustom={false} force-hides it).
|
|
@@ -221,7 +276,13 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
221
276
|
const effectiveAllowCustom = allowCustom ?? customCfg?.enabled ?? false;
|
|
222
277
|
const effectiveCustomMin = customMin ?? customCfg?.minEur;
|
|
223
278
|
const effectiveCustomMax = customMax ?? customCfg?.maxEur ?? undefined;
|
|
224
|
-
|
|
279
|
+
// Published theme → --crediball-* CSS vars, applied on a display:contents
|
|
280
|
+
// wrapper so it themes every descendant (and the paywall) without adding a
|
|
281
|
+
// layout box. Host props/CSS are overridden by design (dashboard wins).
|
|
282
|
+
const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
|
|
283
|
+
const hasThemeVars = Object.keys(themeVars).length > 0;
|
|
284
|
+
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, accentColor: accentColor })] }));
|
|
285
|
+
return (_jsx(CrediballContext.Provider, { value: ctx, children: hasThemeVars ? _jsx("div", { style: { display: "contents", ...themeVars }, children: inner }) : inner }));
|
|
225
286
|
}
|
|
226
287
|
/**
|
|
227
288
|
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
package/dist/CreditIndicator.js
CHANGED
|
@@ -7,9 +7,11 @@ import { useCrediballOptional } from "./CrediballProvider";
|
|
|
7
7
|
* Pattern C — Subtle indicator. A quiet balance chip for a host app's top bar;
|
|
8
8
|
* clickable to open a top-up flow. Minimal and non-intrusive.
|
|
9
9
|
*/
|
|
10
|
-
export function CreditIndicator({ balanceLabel, label
|
|
10
|
+
export function CreditIndicator({ balanceLabel, label, onClick, accentColor, style, }) {
|
|
11
11
|
const ctx = useCrediballOptional();
|
|
12
12
|
const resolvedLabel = balanceLabel ?? (ctx?.balance != null ? formatCredits(ctx.balance) : "—");
|
|
13
|
+
// Prefer the host prop, then the dashboard-published default, then the built-in.
|
|
14
|
+
const resolvedText = label ?? ctx?.content?.indicatorLabel ?? "Credits:";
|
|
13
15
|
const resolvedOnClick = onClick ?? ctx?.openPaywall;
|
|
14
16
|
return (_jsxs("button", { onClick: resolvedOnClick, style: {
|
|
15
17
|
display: "inline-flex",
|
|
@@ -26,5 +28,5 @@ export function CreditIndicator({ balanceLabel, label = "Credits:", onClick, acc
|
|
|
26
28
|
padding: "4px 12px",
|
|
27
29
|
...accentStyle(accentColor),
|
|
28
30
|
...style,
|
|
29
|
-
}, children: [_jsx("span", { style: { color: theme.inkMuted }, children:
|
|
31
|
+
}, children: [_jsx("span", { style: { color: theme.inkMuted }, children: resolvedText }), _jsx("span", { style: { fontWeight: 600 }, children: resolvedLabel })] }));
|
|
30
32
|
}
|
package/dist/CreditsBadge.js
CHANGED
|
@@ -9,9 +9,12 @@ import { useCrediballOptional } from "./CrediballProvider";
|
|
|
9
9
|
* as the primary figure and an approximate money value beneath it. No Crediball
|
|
10
10
|
* branding.
|
|
11
11
|
*/
|
|
12
|
-
export function CreditsBadge({ credits, rate, currency = "EUR", label
|
|
12
|
+
export function CreditsBadge({ credits, rate, currency = "EUR", label, onAddCredits, addLabel, accentColor, style, }) {
|
|
13
13
|
const ctx = useCrediballOptional();
|
|
14
14
|
const resolvedCredits = credits ?? ctx?.balance ?? 0;
|
|
15
|
+
// Prefer the host prop, then the dashboard-published default, then the built-in.
|
|
16
|
+
const resolvedLabel = label ?? ctx?.content?.badgeLabel ?? "Credit balance";
|
|
17
|
+
const resolvedAddLabel = addLabel ?? ctx?.content?.badgeAddLabel ?? "Add credits";
|
|
15
18
|
const showMoney = typeof rate === "number" && rate > 0;
|
|
16
19
|
return (_jsxs("div", { style: {
|
|
17
20
|
display: "flex",
|
|
@@ -25,7 +28,7 @@ export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit
|
|
|
25
28
|
fontFamily: theme.font,
|
|
26
29
|
...accentStyle(accentColor),
|
|
27
30
|
...style,
|
|
28
|
-
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, color: theme.inkMuted }, children:
|
|
31
|
+
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, color: theme.inkMuted }, children: resolvedLabel }), _jsxs("div", { style: { fontSize: 28, fontWeight: 600, color: theme.ink, letterSpacing: "-0.01em" }, children: [formatCredits(resolvedCredits), _jsx("span", { style: { fontSize: 16, fontWeight: 400, color: theme.inkMuted, marginLeft: 6 }, children: "credits" })] }), showMoney ? (_jsxs("div", { style: { fontSize: 13, color: theme.inkMuted, marginTop: 2 }, children: ["\u2248 ", formatMoney(resolvedCredits / rate, currency)] })) : null] }), _jsx("button", { onClick: onAddCredits ?? ctx?.openPaywall, style: {
|
|
29
32
|
appearance: "none",
|
|
30
33
|
border: "none",
|
|
31
34
|
cursor: "pointer",
|
|
@@ -35,5 +38,5 @@ export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit
|
|
|
35
38
|
fontSize: 14,
|
|
36
39
|
padding: "8px 16px",
|
|
37
40
|
borderRadius: theme.radiusPill,
|
|
38
|
-
}, children:
|
|
41
|
+
}, children: resolvedAddLabel })] }));
|
|
39
42
|
}
|
|
@@ -8,13 +8,16 @@ import { useCrediball } from "../CrediballProvider";
|
|
|
8
8
|
*
|
|
9
9
|
* <CrediballLowCreditWarning threshold={20} message="Not enough for one more image." />
|
|
10
10
|
*/
|
|
11
|
-
export function CrediballLowCreditWarning({ className, style, threshold, message
|
|
12
|
-
const { balance, isLow: contextIsLow } = useCrediball();
|
|
13
|
-
|
|
11
|
+
export function CrediballLowCreditWarning({ className, style, threshold, message, children, }) {
|
|
12
|
+
const { balance, isLow: contextIsLow, content } = useCrediball();
|
|
13
|
+
// Prefer the host prop, then the dashboard-published default, then the provider's.
|
|
14
|
+
const effectiveThreshold = threshold ?? content?.lowCreditThreshold;
|
|
15
|
+
const isLow = effectiveThreshold != null && balance != null ? balance < effectiveThreshold : contextIsLow;
|
|
14
16
|
if (!isLow)
|
|
15
17
|
return null;
|
|
16
18
|
if (typeof children === "function") {
|
|
17
19
|
return _jsx(_Fragment, { children: children({ balance, isLow }) });
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
const resolvedMessage = message ?? content?.lowCreditMessage ?? "Low credit balance — top up to keep going.";
|
|
22
|
+
return (_jsx("span", { className: className, style: { font: "inherit", color: "currentColor", ...style }, children: children ?? resolvedMessage }));
|
|
20
23
|
}
|
|
@@ -11,7 +11,7 @@ import { theme, accentStyle } from "../theme";
|
|
|
11
11
|
* <CrediballTopUpButton>Get more credits</CrediballTopUpButton>
|
|
12
12
|
*/
|
|
13
13
|
export function CrediballTopUpButton({ className, style, children, onClick, accentColor }) {
|
|
14
|
-
const { openPaywall } = useCrediball();
|
|
14
|
+
const { openPaywall, content } = useCrediball();
|
|
15
15
|
return (_jsx("button", { type: "button", className: className, onClick: onClick ?? openPaywall, style: {
|
|
16
16
|
font: "inherit",
|
|
17
17
|
appearance: "none",
|
|
@@ -23,5 +23,5 @@ export function CrediballTopUpButton({ className, style, children, onClick, acce
|
|
|
23
23
|
padding: "0.5em 1.1em",
|
|
24
24
|
...accentStyle(accentColor),
|
|
25
25
|
...style,
|
|
26
|
-
}, children: children ?? "Add credits" }));
|
|
26
|
+
}, children: children ?? content?.topUpButtonText ?? "Add credits" }));
|
|
27
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediball/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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.
|
|
54
|
+
"@crediball/core": "^0.3.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "^18.3.11",
|