@crediball/react 0.17.0 → 0.19.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, type PackagesPayload, type CrediballUiContent } from "@crediball/core";
3
+ import { type UsagePeriod, type PackagesPayload, type CrediballUiContent, type ColorScheme } from "@crediball/core";
4
4
  export interface CrediballProviderProps {
5
5
  children: ReactNode;
6
6
  /**
@@ -22,8 +22,12 @@ export interface CrediballProviderProps {
22
22
  lowCreditThreshold?: number;
23
23
  /**
24
24
  * Top-up amounts to offer in the paywall (host decides the meaning, e.g. euros).
25
- * Match these to the packages you configured in the Crediball dashboard.
26
- * Ignored once real packages are available (with publishableKey/userId set).
25
+ * Only used in data-less mode (no publishableKey/userId), where there's no
26
+ * dashboard catalog to fall back to and the host is fully responsible for
27
+ * `onTopup`. Ignored whenever publishableKey + userId are set — the paywall
28
+ * then shows your dashboard's real packages, or no top-up button at all if
29
+ * you haven't configured any yet, rather than a button that can't actually
30
+ * charge anything.
27
31
  */
28
32
  amounts?: number[];
29
33
  /**
@@ -88,7 +92,7 @@ export interface CrediballProviderProps {
88
92
  customMax?: number;
89
93
  /**
90
94
  * Show a "Have a promo code?" field in the paywall. Auto-derived from your
91
- * dashboard's Growth → Promotions config: shown automatically once you have
95
+ * dashboard's Promos → Promotions config: shown automatically once you have
92
96
  * an active Checkout promotion, same as `allowCustom`/custom top-up. Pass
93
97
  * this only to override (e.g. `false` to force-hide it, or `true` to show it
94
98
  * before you've set one up). The code is passed through to
@@ -118,6 +122,16 @@ export interface CrediballProviderProps {
118
122
  * theme entirely from your own stylesheet.
119
123
  */
120
124
  applyRemoteTheme?: boolean;
125
+ /**
126
+ * Which color scheme to render the published theme's dark colors in (if
127
+ * the developer has configured a dark palette on the dashboard). Defaults
128
+ * to `"system"` — follows the OS/browser's `prefers-color-scheme` and
129
+ * updates live if it changes. Pass `"light"`/`"dark"` explicitly to follow
130
+ * your own app's in-app theme toggle instead (common when it can diverge
131
+ * from the OS setting). Read back via `useCrediball().colorScheme` for
132
+ * your own custom UI.
133
+ */
134
+ colorScheme?: "light" | "dark" | "system";
121
135
  /**
122
136
  * Automatic referral handling. On by default: any visit with `?ref=CODE` in
123
137
  * the URL is captured (stored locally + click counted), and once a `userId`
@@ -177,6 +191,8 @@ interface CrediballContextValue {
177
191
  currencySymbol: string;
178
192
  /** Developer-published default copy from the dashboard, used as a label fallback. null when unconfigured. */
179
193
  content: CrediballUiContent | null;
194
+ /** The resolved color scheme (`colorScheme` prop, or the live OS/browser preference when it's "system"). */
195
+ colorScheme: ColorScheme;
180
196
  }
181
197
  /**
182
198
  * Pattern B, automated. Wrap your app once:
@@ -198,7 +214,7 @@ interface CrediballContextValue {
198
214
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
199
215
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
200
216
  */
201
- export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch, allowCustom, customMin, customMax, allowPromoCode, promoCodeLabel, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
217
+ export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch, allowCustom, customMin, customMax, allowPromoCode, promoCodeLabel, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, colorScheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
202
218
  /**
203
219
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
204
220
  *
@@ -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, fetchReferral, } from "@crediball/core";
7
+ import { getCrediballClient, notifyBalanceChanged, loadRemoteFont, themeToCssVars, getSystemColorScheme, watchSystemColorScheme, 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,13 +63,19 @@ 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, onEnableAutoTopup, watchFetch = true, allowCustom, customMin, customMax, allowPromoCode, promoCodeLabel, 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, allowPromoCode, promoCodeLabel, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme = true, colorScheme = "system", captureReferrals = true, }) {
67
67
  const [open, setOpen] = useState(false);
68
68
  // Surfaced in the paywall itself (not just the console) when the built-in
69
69
  // checkout fails — e.g. an invalid/expired promo code — so a user isn't
70
70
  // left clicking a "dead" button with no visible feedback. Cleared at the
71
71
  // start of every attempt so it always reflects the latest one.
72
72
  const [checkoutError, setCheckoutError] = useState(null);
73
+ // Tracks the OS/browser's live prefers-color-scheme, only consulted when
74
+ // colorScheme is "system" (the default) — an explicit "light"/"dark" prop
75
+ // always wins, for apps whose own toggle can diverge from the OS setting.
76
+ const [systemColorScheme, setSystemColorScheme] = useState(getSystemColorScheme);
77
+ useEffect(() => watchSystemColorScheme(setSystemColorScheme), []);
78
+ const resolvedColorScheme = colorScheme === "system" ? systemColorScheme : colorScheme;
73
79
  // Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
74
80
  // count the click) as soon as the provider mounts — the visitor usually has
75
81
  // no account yet at this point.
@@ -180,7 +186,20 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
180
186
  packages: pkgs,
181
187
  currencySymbol,
182
188
  content: uiContent,
183
- }), [showPaywall, hidePaywall, open, snapshot, costOf, topUp, refresh, pkgs, currencySymbol, uiContent]);
189
+ colorScheme: resolvedColorScheme,
190
+ }), [
191
+ showPaywall,
192
+ hidePaywall,
193
+ open,
194
+ snapshot,
195
+ costOf,
196
+ topUp,
197
+ refresh,
198
+ pkgs,
199
+ currencySymbol,
200
+ uiContent,
201
+ resolvedColorScheme,
202
+ ]);
184
203
  // The page to return to after Stripe checkout — the one the user is on now.
185
204
  function currentReturnPath() {
186
205
  if (typeof window === "undefined")
@@ -272,14 +291,20 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
272
291
  const effectiveCustomMin = customMin ?? customCfg?.minEur;
273
292
  const effectiveCustomMax = customMax ?? customCfg?.maxEur ?? undefined;
274
293
  // Same auto-derivation for the promo-code field: a developer who set up a
275
- // Checkout promotion in Growth gets the field for free, no code change.
294
+ // Checkout promotion in Promos gets the field for free, no code change.
276
295
  const effectiveAllowPromoCode = allowPromoCode ?? pkgs?.hasActiveCheckoutPromotion ?? false;
277
296
  // Published theme → --crediball-* CSS vars, applied on a display:contents
278
297
  // wrapper so it themes every descendant (and the paywall) without adding a
279
298
  // layout box. Host props/CSS are overridden by design (dashboard wins).
280
- const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
299
+ const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme, resolvedColorScheme) : {};
281
300
  const hasThemeVars = Object.keys(themeVars).length > 0;
282
- 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, allowPromoCode: effectiveAllowPromoCode, promoCodeLabel: promoCodeLabel, customTopupRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, 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, referredReward: referral?.referredRewardCredits, referralLabel: uiContent?.referralLabel, checkoutError: checkoutError, accentColor: accentColor })] }));
301
+ 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,
302
+ // Connected to live dashboard data: the legacy flat-amounts fallback
303
+ // only makes sense when there's no catalog to consult at all (see the
304
+ // `amounts` prop doc). Once a client exists, no packages configured
305
+ // means no top-up button — never a button whose checkout is
306
+ // guaranteed to fail.
307
+ amounts: client ? [] : amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: effectiveAllowCustom, customMin: effectiveCustomMin, customMax: effectiveCustomMax, allowPromoCode: effectiveAllowPromoCode, promoCodeLabel: promoCodeLabel, customTopupRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, 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, referredReward: referral?.referredRewardCredits, referralLabel: uiContent?.referralLabel, checkoutError: checkoutError, accentColor: accentColor })] }));
283
308
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
284
309
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
285
310
  }
@@ -217,7 +217,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
217
217
  ? "You've used all your credits for this period."
218
218
  : title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize: 13, color: theme.inkMuted }, children: activeSubscription
219
219
  ? `Your ${activeSubscription.planName} credits renew on ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()}.`
220
- : description }), checkoutError ? (_jsx("p", { style: { marginTop: 12, marginBottom: 0, fontSize: 13, color: theme.accent }, children: checkoutError })) : null, activeSubscription && (_jsxs("div", { style: {
220
+ : description }), activeSubscription && (_jsxs("div", { style: {
221
221
  marginTop: 16,
222
222
  padding: "12px 16px",
223
223
  borderRadius: theme.radiusCard,
@@ -272,7 +272,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
272
272
  color: theme.inkMuted,
273
273
  fontFamily: theme.font,
274
274
  textDecoration: "underline",
275
- }, children: promoCodeLabel })) })), 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"
275
+ }, children: promoCodeLabel })) })), checkoutError ? (_jsx("p", { style: { marginTop: 12, marginBottom: 0, fontSize: 13, color: theme.accent }, children: checkoutError })) : null, 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"
276
276
  ? "Auto top-up needs you to re-confirm your payment method."
277
277
  : "Auto top-up's last charge failed." }), _jsx("button", { onClick: () => onEnableAutoTopup?.({
278
278
  packageId: autoTopup.packageId,
@@ -13,6 +13,14 @@ export interface CrediballLowCreditWarningProps {
13
13
  threshold?: number;
14
14
  /** Message shown when low. Defaults to a generic nudge. */
15
15
  message?: string;
16
+ /**
17
+ * Preview/demo override — forces the warning to render regardless of the
18
+ * live balance, the same way `PaywallModal`'s balance prop and
19
+ * `ReferralCard`'s `demo` prop let the dashboard's UI-patterns preview show
20
+ * a component with no connected app. Never pass this in production; the
21
+ * component is meant to gate on the real live balance.
22
+ */
23
+ demo?: boolean;
16
24
  children?: ReactNode | ((props: CrediballLowCreditWarningRenderProps) => ReactNode);
17
25
  }
18
26
  /**
@@ -22,4 +30,4 @@ export interface CrediballLowCreditWarningProps {
22
30
  *
23
31
  * <CrediballLowCreditWarning threshold={20} message="Not enough for one more image." />
24
32
  */
25
- export declare function CrediballLowCreditWarning({ className, style, threshold, message, children, }: CrediballLowCreditWarningProps): import("react").JSX.Element | null;
33
+ export declare function CrediballLowCreditWarning({ className, style, threshold, message, demo, children, }: CrediballLowCreditWarningProps): import("react").JSX.Element | null;
@@ -10,11 +10,11 @@ import { InlineValue } from "../InlineValue";
10
10
  *
11
11
  * <CrediballLowCreditWarning threshold={20} message="Not enough for one more image." />
12
12
  */
13
- export function CrediballLowCreditWarning({ className, style, threshold, message, children, }) {
13
+ export function CrediballLowCreditWarning({ className, style, threshold, message, demo = false, children, }) {
14
14
  const { balance, isLow: contextIsLow, content } = useCrediball();
15
15
  // Prefer the host prop, then the dashboard-published default, then the provider's.
16
16
  const effectiveThreshold = threshold ?? content?.lowCreditThreshold;
17
- const isLow = isBalanceLow(balance, effectiveThreshold, contextIsLow);
17
+ const isLow = demo || isBalanceLow(balance, effectiveThreshold, contextIsLow);
18
18
  if (!isLow)
19
19
  return null;
20
20
  if (typeof children === "function") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.17.0",
3
+ "version": "0.19.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.9.0"
54
+ "@crediball/core": "^0.10.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",