@crediball/react 0.9.0 → 0.10.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.
@@ -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() {
@@ -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.
@@ -230,7 +252,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
230
252
  // layout box. Host props/CSS are overridden by design (dashboard wins).
231
253
  const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
232
254
  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 })] }));
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 })] }));
234
256
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
235
257
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
236
258
  }
@@ -96,6 +96,15 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
96
96
  usageLabel?: string;
97
97
  /** Hide the "Powered by Crediball" footer line (default false). */
98
98
  hideBranding?: boolean;
99
+ /**
100
+ * The user's referral invite link. When provided, a "Refer a friend" row with
101
+ * a copyable link is shown at the bottom of the modal — a way to earn credits
102
+ * without paying. Wired automatically by <CrediballProvider> when referrals are
103
+ * enabled in the dashboard; pass it yourself when using the modal standalone.
104
+ */
105
+ referralLink?: string | null;
106
+ /** Credits earned per successful referral, shown in the refer-a-friend blurb. */
107
+ referralReward?: number;
99
108
  style?: CSSProperties;
100
109
  }
101
110
  /**
@@ -103,4 +112,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
103
112
  * continue. The host controls when it appears; shows a small "Powered by
104
113
  * Crediball" line by default (set `hideBranding` to remove it).
105
114
  */
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;
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;
@@ -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, 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, }) {
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,7 @@ 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);
34
35
  useEffect(() => {
35
36
  const el = overlayRef.current;
36
37
  if (!el || typeof ResizeObserver === "undefined")
@@ -56,6 +57,13 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
56
57
  setCancelPending(false);
57
58
  }
58
59
  }
60
+ async function copyReferral() {
61
+ if (!referralLink || typeof navigator === "undefined" || !navigator.clipboard)
62
+ return;
63
+ await navigator.clipboard.writeText(referralLink);
64
+ setReferralCopied(true);
65
+ setTimeout(() => setReferralCopied(false), 2000);
66
+ }
59
67
  function submitCustom() {
60
68
  const amt = Number(customValue);
61
69
  if (!Number.isFinite(amt) || amt <= 0) {
@@ -162,7 +170,37 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
162
170
  color: theme.ink,
163
171
  background: theme.canvas,
164
172
  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: {
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,
175
+ paddingTop: 16,
176
+ 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: {
180
+ flex: 1,
181
+ minWidth: 0,
182
+ overflow: "hidden",
183
+ textOverflow: "ellipsis",
184
+ whiteSpace: "nowrap",
185
+ fontSize: 13,
186
+ color: theme.ink,
187
+ border: `1px solid ${theme.hairline}`,
188
+ borderRadius: theme.radiusPill,
189
+ padding: "8px 14px",
190
+ }, children: referralLink }), _jsx("button", { onClick: copyReferral, style: {
191
+ flex: "none",
192
+ appearance: "none",
193
+ cursor: "pointer",
194
+ background: "transparent",
195
+ color: theme.ink,
196
+ border: `1px solid ${theme.hairline}`,
197
+ fontFamily: theme.font,
198
+ fontWeight: 500,
199
+ fontSize: 14,
200
+ padding: "8px 16px",
201
+ borderRadius: theme.radiusPill,
202
+ whiteSpace: "nowrap",
203
+ }, children: referralCopied ? "Copied!" : "Copy" })] })] })) : null, onClose ? (_jsx("button", { onClick: onClose, style: {
166
204
  width: "100%",
167
205
  marginTop: 12,
168
206
  appearance: "none",
@@ -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.10.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>",