@crediball/react 0.9.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
  *
@@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
4
4
  import { PaywallModal } from "./PaywallModal";
5
5
  import { subscribeFetchWatcher } from "./fetchWatcher";
6
6
  import { subscribeInsufficientCredits } from "./creditEvent";
7
- import { getCrediballClient, notifyBalanceChanged, loadRemoteFont, themeToCssVars, captureReferral, completeStoredReferral, } from "@crediball/core";
7
+ import { getCrediballClient, notifyBalanceChanged, loadRemoteFont, themeToCssVars, captureReferral, completeStoredReferral, fetchReferral, } from "@crediball/core";
8
8
  const CrediballConfigContext = createContext(null);
9
9
  /** Internal hook: the provider's publishableKey/userId/apiUrl. Throws outside a provider. */
10
10
  export function useCrediballConfig() {
@@ -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
@@ -82,6 +82,28 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
82
82
  return;
83
83
  void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => { });
84
84
  }, [captureReferrals, publishableKey, userId, apiUrl]);
85
+ // The user's own referral invite link + reward, for the paywall's
86
+ // "Refer a friend" row. Null while loading or when referrals are disabled.
87
+ const [referral, setReferral] = useState(null);
88
+ useEffect(() => {
89
+ if (!publishableKey || !userId) {
90
+ setReferral(null);
91
+ return;
92
+ }
93
+ let cancelled = false;
94
+ fetchReferral({ publishableKey, userId, apiUrl })
95
+ .then((data) => {
96
+ if (!cancelled)
97
+ setReferral(data.enabled ? data : null);
98
+ })
99
+ .catch(() => {
100
+ if (!cancelled)
101
+ setReferral(null);
102
+ });
103
+ return () => {
104
+ cancelled = true;
105
+ };
106
+ }, [publishableKey, userId, apiUrl]);
85
107
  const showPaywall = useCallback(() => setOpen(true), []);
86
108
  const hidePaywall = useCallback(() => setOpen(false), []);
87
109
  // Keep a ref so the fetch-watcher closure always calls the latest version.
@@ -218,6 +240,18 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
218
240
  await onCancelSubscription?.(subscriptionId);
219
241
  await client?.refresh();
220
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
+ }
221
255
  // Auto-derive the custom-amount field from the dashboard's custom-topup config,
222
256
  // so a developer who enabled it doesn't also have to pass allowCustom/min/max.
223
257
  // Explicit props still win (allowCustom={false} force-hides it).
@@ -230,7 +264,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
230
264
  // layout box. Host props/CSS are overridden by design (dashboard wins).
231
265
  const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
232
266
  const hasThemeVars = Object.keys(themeVars).length > 0;
233
- 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 })] }));
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 })] }));
234
268
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
235
269
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
236
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.
@@ -96,6 +121,21 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
96
121
  usageLabel?: string;
97
122
  /** Hide the "Powered by Crediball" footer line (default false). */
98
123
  hideBranding?: boolean;
124
+ /**
125
+ * The user's referral invite link. When provided, a "Refer a friend" row with
126
+ * a copyable link is shown at the bottom of the modal — a way to earn credits
127
+ * without paying. Wired automatically by <CrediballProvider> when referrals are
128
+ * enabled in the dashboard; pass it yourself when using the modal standalone.
129
+ */
130
+ referralLink?: string | null;
131
+ /** Credits earned per successful referral, shown in the refer-a-friend blurb. */
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;
99
139
  style?: CSSProperties;
100
140
  }
101
141
  /**
@@ -103,4 +143,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
103
143
  * continue. The host controls when it appears; shows a small "Powered by
104
144
  * Crediball" line by default (set `hideBranding` to remove it).
105
145
  */
106
- 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, 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, 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
@@ -31,6 +31,10 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
31
31
  const [customError, setCustomError] = useState(null);
32
32
  const [cancelPending, setCancelPending] = useState(false);
33
33
  const [cancelDone, setCancelDone] = useState(false);
34
+ const [referralCopied, setReferralCopied] = useState(false);
35
+ const [autoTopupOpen, setAutoTopupOpen] = useState(false);
36
+ const [autoTopupPackageId, setAutoTopupPackageId] = useState("");
37
+ const [autoTopupThreshold, setAutoTopupThreshold] = useState("");
34
38
  useEffect(() => {
35
39
  const el = overlayRef.current;
36
40
  if (!el || typeof ResizeObserver === "undefined")
@@ -42,6 +46,15 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
42
46
  observer.observe(el);
43
47
  return () => observer.disconnect();
44
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]);
45
58
  if (!open)
46
59
  return null;
47
60
  async function handleCancelSubscription() {
@@ -56,6 +69,13 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
56
69
  setCancelPending(false);
57
70
  }
58
71
  }
72
+ async function copyReferral() {
73
+ if (!referralLink || typeof navigator === "undefined" || !navigator.clipboard)
74
+ return;
75
+ await navigator.clipboard.writeText(referralLink);
76
+ setReferralCopied(true);
77
+ setTimeout(() => setReferralCopied(false), 2000);
78
+ }
59
79
  function submitCustom() {
60
80
  const amt = Number(customValue);
61
81
  if (!Number.isFinite(amt) || amt <= 0) {
@@ -73,6 +93,12 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
73
93
  setCustomError(null);
74
94
  onAdd?.(amt);
75
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
+ }
76
102
  const buttonStyle = {
77
103
  flex: 1,
78
104
  appearance: "none",
@@ -87,6 +113,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
87
113
  padding: "8px 16px",
88
114
  borderRadius: theme.radiusPill,
89
115
  };
116
+ const resolvedReferralLabel = referralLabel.replace("{credits}", formatCredits(referralReward ?? 0));
90
117
  return (_jsx("div", { ref: overlayRef, role: "dialog", "aria-modal": "true", style: {
91
118
  position: "fixed",
92
119
  inset: 0,
@@ -162,7 +189,52 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
162
189
  color: theme.ink,
163
190
  background: theme.canvas,
164
191
  outline: "none",
165
- } }), _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, onClose ? (_jsx("button", { onClick: onClose, style: {
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: {
166
238
  width: "100%",
167
239
  marginTop: 12,
168
240
  appearance: "none",
@@ -174,5 +246,38 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
174
246
  fontSize: 12,
175
247
  padding: "8px 16px",
176
248
  borderRadius: theme.radiusPill,
177
- }, 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." }) }))] }) }));
249
+ }, children: "Not now" })) : null, referralLink ? (_jsxs("div", { style: {
250
+ marginTop: 16,
251
+ paddingTop: 16,
252
+ borderTop: `1px solid ${theme.hairline}`,
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: {
259
+ flex: 1,
260
+ minWidth: 0,
261
+ overflow: "hidden",
262
+ textOverflow: "ellipsis",
263
+ whiteSpace: "nowrap",
264
+ fontSize: 13,
265
+ color: theme.ink,
266
+ border: `1px solid ${theme.hairline}`,
267
+ borderRadius: theme.radiusPill,
268
+ padding: "8px 14px",
269
+ }, children: referralLink }), _jsx("button", { onClick: copyReferral, style: {
270
+ flex: "none",
271
+ appearance: "none",
272
+ cursor: "pointer",
273
+ background: "transparent",
274
+ color: theme.ink,
275
+ border: `1px solid ${theme.hairline}`,
276
+ fontFamily: theme.font,
277
+ fontWeight: 500,
278
+ fontSize: 14,
279
+ padding: "8px 16px",
280
+ borderRadius: theme.radiusPill,
281
+ whiteSpace: "nowrap",
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." }) }))] }) }));
178
283
  }
@@ -15,6 +15,25 @@ export interface ReferralCardProps extends CrediballThemeOverrides {
15
15
  /** Hide the clicks / converted counters row. */
16
16
  hideStats?: boolean;
17
17
  style?: CSSProperties;
18
+ /**
19
+ * Preview/demo override. When set, the card renders from this sample data
20
+ * instead of the live `useReferral()` result — used by the dashboard's
21
+ * UI-patterns preview so the card is visible and themeable without a
22
+ * connected app, exactly like `PaywallModal`'s balance/packages preview
23
+ * props. In production you never pass this: the card fetches real data itself.
24
+ */
25
+ demo?: {
26
+ link?: string | null;
27
+ code?: string;
28
+ clicks?: number;
29
+ rewardCredits?: number;
30
+ referredRewardCredits?: number;
31
+ rewards?: {
32
+ pending: number;
33
+ converted: number;
34
+ creditsEarned: number;
35
+ };
36
+ };
18
37
  }
19
38
  /**
20
39
  * Drop-in invite card built on useReferral(). Place anywhere inside
@@ -27,4 +46,4 @@ export interface ReferralCardProps extends CrediballThemeOverrides {
27
46
  * loading or when the referral program is disabled in the dashboard, and is
28
47
  * themed by the same `--crediball-*` variables as every other component.
29
48
  */
30
- export declare function ReferralCard({ title, description, copyLabel, shareLabel, hideStats, accentColor, style, }: ReferralCardProps): import("react").JSX.Element | null;
49
+ export declare function ReferralCard({ title, description, copyLabel, shareLabel, hideStats, accentColor, style, demo, }: ReferralCardProps): import("react").JSX.Element | null;
@@ -14,8 +14,21 @@ import { useReferral } from "./useReferral";
14
14
  * loading or when the referral program is disabled in the dashboard, and is
15
15
  * themed by the same `--crediball-*` variables as every other component.
16
16
  */
17
- export function ReferralCard({ title = "Invite friends", description, copyLabel = "Copy", shareLabel = "Share", hideStats = false, accentColor, style, }) {
18
- const { enabled, link, code, clicks, rewards, rewardCredits, referredRewardCredits, loading, copy, copied, share, } = useReferral();
17
+ export function ReferralCard({ title = "Invite friends", description, copyLabel = "Copy", shareLabel = "Share", hideStats = false, accentColor, style, demo, }) {
18
+ const live = useReferral();
19
+ // Demo/preview data (if any) overrides the live hook result and bypasses the
20
+ // loading/enabled gate, so the card renders in the UI-patterns preview.
21
+ const enabled = demo ? true : live.enabled;
22
+ const link = demo ? (demo.link ?? null) : live.link;
23
+ const code = demo ? (demo.code ?? null) : live.code;
24
+ const clicks = demo ? (demo.clicks ?? 0) : live.clicks;
25
+ const rewards = demo?.rewards ?? live.rewards;
26
+ const rewardCredits = demo ? (demo.rewardCredits ?? 0) : live.rewardCredits;
27
+ const referredRewardCredits = demo ? (demo.referredRewardCredits ?? 0) : live.referredRewardCredits;
28
+ const loading = demo ? false : live.loading;
29
+ const copied = demo ? false : live.copied;
30
+ const copy = live.copy;
31
+ const share = live.share;
19
32
  if (loading || !enabled || !code)
20
33
  return null;
21
34
  const resolvedDescription = description ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.9.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",