@crediball/react 0.16.0 → 0.18.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
  /**
@@ -118,6 +118,16 @@ export interface CrediballProviderProps {
118
118
  * theme entirely from your own stylesheet.
119
119
  */
120
120
  applyRemoteTheme?: boolean;
121
+ /**
122
+ * Which color scheme to render the published theme's dark colors in (if
123
+ * the developer has configured a dark palette on the dashboard). Defaults
124
+ * to `"system"` — follows the OS/browser's `prefers-color-scheme` and
125
+ * updates live if it changes. Pass `"light"`/`"dark"` explicitly to follow
126
+ * your own app's in-app theme toggle instead (common when it can diverge
127
+ * from the OS setting). Read back via `useCrediball().colorScheme` for
128
+ * your own custom UI.
129
+ */
130
+ colorScheme?: "light" | "dark" | "system";
121
131
  /**
122
132
  * Automatic referral handling. On by default: any visit with `?ref=CODE` in
123
133
  * the URL is captured (stored locally + click counted), and once a `userId`
@@ -177,6 +187,8 @@ interface CrediballContextValue {
177
187
  currencySymbol: string;
178
188
  /** Developer-published default copy from the dashboard, used as a label fallback. null when unconfigured. */
179
189
  content: CrediballUiContent | null;
190
+ /** The resolved color scheme (`colorScheme` prop, or the live OS/browser preference when it's "system"). */
191
+ colorScheme: ColorScheme;
180
192
  }
181
193
  /**
182
194
  * Pattern B, automated. Wrap your app once:
@@ -198,7 +210,7 @@ interface CrediballContextValue {
198
210
  * SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
199
211
  * the watcher scans each chunk for credit errors, so no manual wiring is needed.
200
212
  */
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;
213
+ 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
214
  /**
203
215
  * Access the paywall + live data from anywhere inside <CrediballProvider>.
204
216
  *
@@ -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,8 +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
+ // Surfaced in the paywall itself (not just the console) when the built-in
69
+ // checkout fails — e.g. an invalid/expired promo code — so a user isn't
70
+ // left clicking a "dead" button with no visible feedback. Cleared at the
71
+ // start of every attempt so it always reflects the latest one.
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;
68
79
  // Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
69
80
  // count the click) as soon as the provider mounts — the visitor usually has
70
81
  // no account yet at this point.
@@ -104,7 +115,10 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
104
115
  cancelled = true;
105
116
  };
106
117
  }, [publishableKey, userId, apiUrl]);
107
- const showPaywall = useCallback(() => setOpen(true), []);
118
+ const showPaywall = useCallback(() => {
119
+ setCheckoutError(null);
120
+ setOpen(true);
121
+ }, []);
108
122
  const hidePaywall = useCallback(() => setOpen(false), []);
109
123
  // Keep a ref so the fetch-watcher closure always calls the latest version.
110
124
  const showPaywallRef = useRef(showPaywall);
@@ -172,7 +186,20 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
172
186
  packages: pkgs,
173
187
  currencySymbol,
174
188
  content: uiContent,
175
- }), [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
+ ]);
176
203
  // The page to return to after Stripe checkout — the one the user is on now.
177
204
  function currentReturnPath() {
178
205
  if (typeof window === "undefined")
@@ -209,11 +236,13 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
209
236
  return;
210
237
  }
211
238
  if (client) {
239
+ setCheckoutError(null);
212
240
  try {
213
241
  await builtIn();
214
242
  }
215
243
  catch (err) {
216
244
  console.error(`[crediball] ${errorLabel} failed:`, err);
245
+ setCheckoutError(err instanceof Error ? err.message : `Could not start ${errorLabel}. Please try again.`);
217
246
  }
218
247
  return;
219
248
  }
@@ -267,9 +296,9 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
267
296
  // Published theme → --crediball-* CSS vars, applied on a display:contents
268
297
  // wrapper so it themes every descendant (and the paywall) without adding a
269
298
  // layout box. Host props/CSS are overridden by design (dashboard wins).
270
- const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
299
+ const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme, resolvedColorScheme) : {};
271
300
  const hasThemeVars = Object.keys(themeVars).length > 0;
272
- 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, 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, 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 })] }));
273
302
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
274
303
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
275
304
  }
@@ -100,6 +100,13 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
100
100
  * the promo code the user entered, same as `onSelectPackage`'s. */
101
101
  onAdd?: (amount: number, code?: string) => void;
102
102
  onClose?: () => void;
103
+ /**
104
+ * An error from the last checkout attempt (e.g. "Invalid or expired promo
105
+ * code.") to show inline, so a failed purchase isn't silently invisible.
106
+ * Wired automatically by <CrediballProvider> when its built-in checkout
107
+ * fails; pass it yourself when using the modal standalone.
108
+ */
109
+ checkoutError?: string | null;
103
110
  /** When true, show a free-form amount field in addition to the preset buttons. */
104
111
  allowCustom?: boolean;
105
112
  /**
@@ -175,4 +182,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
175
182
  * continue. The host controls when it appears; shows a small "Powered by
176
183
  * Crediball" line by default (set `hideBranding` to remove it).
177
184
  */
178
- export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, allowPromoCode, promoCodeLabel, addButtonText, customMin, customMax, customTopupRate, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referredReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
185
+ export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, checkoutError, allowCustom, allowPromoCode, promoCodeLabel, addButtonText, customMin, customMax, customTopupRate, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referredReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
@@ -24,7 +24,7 @@ function sectionLabelStyle(marginBottom) {
24
24
  * continue. The host controls when it appears; shows a small "Powered by
25
25
  * Crediball" line by default (set `hideBranding` to remove it).
26
26
  */
27
- 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, allowPromoCode = false, promoCodeLabel = "Have a promo code?", addButtonText = "Add", customMin = 1, customMax, customTopupRate, currencySymbol = "€", balance, balanceRate, currency = "EUR", usage, usageLabel = "Usage", hideBranding = false, referralLink, referralReward, referredReward, referralLabel, accentColor, style, }) {
27
+ 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, checkoutError, allowCustom = false, allowPromoCode = false, promoCodeLabel = "Have a promo code?", addButtonText = "Add", customMin = 1, customMax, customTopupRate, currencySymbol = "€", balance, balanceRate, currency = "EUR", usage, usageLabel = "Usage", hideBranding = false, referralLink, referralReward, referredReward, referralLabel, accentColor, style, }) {
28
28
  // Stack the top-up buttons one-per-line when the dialog itself is narrow.
29
29
  // Measured off the dialog's own box (via ResizeObserver) rather than the
30
30
  // window — in normal use the two are the same width, but this also makes
@@ -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.16.0",
3
+ "version": "0.18.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",