@crediball/react 0.8.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.
- package/dist/CrediballProvider.d.ts +21 -1
- package/dist/CrediballProvider.js +80 -108
- package/dist/InlineValue.d.ts +11 -0
- package/dist/InlineValue.js +10 -0
- package/dist/PaywallModal.d.ts +10 -1
- package/dist/PaywallModal.js +40 -2
- package/dist/ReferralCard.d.ts +49 -0
- package/dist/ReferralCard.js +83 -0
- package/dist/format.d.ts +1 -4
- package/dist/format.js +1 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/useReferral.d.ts +42 -0
- package/dist/useReferral.js +92 -0
- package/dist/variables/CrediballBalance.js +2 -1
- package/dist/variables/CrediballLowCreditWarning.js +4 -2
- package/dist/variables/CrediballUsage.js +2 -1
- package/package.json +2 -2
|
@@ -93,7 +93,27 @@ export interface CrediballProviderProps {
|
|
|
93
93
|
* theme entirely from your own stylesheet.
|
|
94
94
|
*/
|
|
95
95
|
applyRemoteTheme?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Automatic referral handling. On by default: any visit with `?ref=CODE` in
|
|
98
|
+
* the URL is captured (stored locally + click counted), and once a `userId`
|
|
99
|
+
* is present the stored code is attached to that user as a pending referral —
|
|
100
|
+
* so with referrals enabled in the dashboard there is zero extra wiring.
|
|
101
|
+
* Rewards are only ever granted server-side when the conversion condition is
|
|
102
|
+
* met. Set false to manage capture/completion yourself via @crediball/core.
|
|
103
|
+
*/
|
|
104
|
+
captureReferrals?: boolean;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Internal: the provider's raw connection config, for hooks (useReferral) that
|
|
108
|
+
* talk to the API directly instead of reading the polled snapshot.
|
|
109
|
+
*/
|
|
110
|
+
interface CrediballConfigValue {
|
|
111
|
+
publishableKey?: string;
|
|
112
|
+
userId?: string;
|
|
113
|
+
apiUrl?: string;
|
|
96
114
|
}
|
|
115
|
+
/** Internal hook: the provider's publishableKey/userId/apiUrl. Throws outside a provider. */
|
|
116
|
+
export declare function useCrediballConfig(): CrediballConfigValue;
|
|
97
117
|
interface CrediballContextValue {
|
|
98
118
|
/**
|
|
99
119
|
* Open the paywall modal. Always safe to call — never guard with "already shown"
|
|
@@ -152,7 +172,7 @@ interface CrediballContextValue {
|
|
|
152
172
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
153
173
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
154
174
|
*/
|
|
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;
|
|
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;
|
|
156
176
|
/**
|
|
157
177
|
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
158
178
|
*
|
|
@@ -4,7 +4,16 @@ 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, } from "@crediball/core";
|
|
7
|
+
import { getCrediballClient, notifyBalanceChanged, loadRemoteFont, themeToCssVars, captureReferral, completeStoredReferral, fetchReferral, } from "@crediball/core";
|
|
8
|
+
const CrediballConfigContext = createContext(null);
|
|
9
|
+
/** Internal hook: the provider's publishableKey/userId/apiUrl. Throws outside a provider. */
|
|
10
|
+
export function useCrediballConfig() {
|
|
11
|
+
const ctx = useContext(CrediballConfigContext);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
throw new Error("This hook must be used inside <CrediballProvider>.");
|
|
14
|
+
}
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
8
17
|
const CrediballContext = createContext(null);
|
|
9
18
|
const EMPTY_SNAPSHOT = {
|
|
10
19
|
balance: null,
|
|
@@ -34,50 +43,6 @@ function symbolForCurrency(iso) {
|
|
|
34
43
|
return iso;
|
|
35
44
|
}
|
|
36
45
|
}
|
|
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
|
-
}
|
|
81
46
|
/**
|
|
82
47
|
* Pattern B, automated. Wrap your app once:
|
|
83
48
|
*
|
|
@@ -98,8 +63,47 @@ function loadRemoteFont(url) {
|
|
|
98
63
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
99
64
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
100
65
|
*/
|
|
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, }) {
|
|
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, }) {
|
|
102
67
|
const [open, setOpen] = useState(false);
|
|
68
|
+
// Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
|
|
69
|
+
// count the click) as soon as the provider mounts — the visitor usually has
|
|
70
|
+
// no account yet at this point.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!captureReferrals || !publishableKey)
|
|
73
|
+
return;
|
|
74
|
+
captureReferral({ publishableKey, apiUrl });
|
|
75
|
+
}, [captureReferrals, publishableKey, apiUrl]);
|
|
76
|
+
// Referral auto-complete: the moment a userId is known (signup finished, or
|
|
77
|
+
// a signed-in user returns), attach any stored code to them as a pending
|
|
78
|
+
// referral. Best-effort — a failure leaves the code stored for a retry on
|
|
79
|
+
// the next mount.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!captureReferrals || !publishableKey || !userId)
|
|
82
|
+
return;
|
|
83
|
+
void completeStoredReferral({ publishableKey, userId, apiUrl }).catch(() => { });
|
|
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]);
|
|
103
107
|
const showPaywall = useCallback(() => setOpen(true), []);
|
|
104
108
|
const hidePaywall = useCallback(() => setOpen(false), []);
|
|
105
109
|
// Keep a ref so the fetch-watcher closure always calls the latest version.
|
|
@@ -186,35 +190,14 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
186
190
|
if (typeof window !== "undefined")
|
|
187
191
|
window.location.assign(url);
|
|
188
192
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
// Shared shape for handleAdd/handleSelectPackage/handleSelectPlan: a host-supplied
|
|
194
|
+
// handler always wins (and refreshes balance/usage on completion); otherwise fall
|
|
195
|
+
// back to the built-in Stripe checkout when a data client is available, leaving the
|
|
196
|
+
// modal open on failure so the error stays visible; otherwise just close the modal.
|
|
197
|
+
async function runPaywallAction(hostHandler, arg, builtIn, errorLabel) {
|
|
198
|
+
if (hostHandler) {
|
|
192
199
|
try {
|
|
193
|
-
await
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
setOpen(false);
|
|
197
|
-
}
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
// Otherwise start Stripe ourselves (custom amount). Leave the modal open on
|
|
201
|
-
// failure so the error is visible instead of the modal silently closing.
|
|
202
|
-
if (client) {
|
|
203
|
-
try {
|
|
204
|
-
await startBuiltInCheckout({ eurAmount: amount });
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
console.error("[crediball] checkout failed:", err);
|
|
208
|
-
}
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
// Data-less mode with no handler: nothing to charge against.
|
|
212
|
-
setOpen(false);
|
|
213
|
-
}
|
|
214
|
-
async function handleSelectPackage(pkg) {
|
|
215
|
-
if (onSelectPackage) {
|
|
216
|
-
try {
|
|
217
|
-
await onSelectPackage(pkg);
|
|
200
|
+
await hostHandler(arg);
|
|
218
201
|
notifyBalanceChanged();
|
|
219
202
|
await client?.refresh();
|
|
220
203
|
}
|
|
@@ -225,45 +208,33 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
225
208
|
}
|
|
226
209
|
if (client) {
|
|
227
210
|
try {
|
|
228
|
-
await
|
|
211
|
+
await builtIn();
|
|
229
212
|
}
|
|
230
213
|
catch (err) {
|
|
231
|
-
console.error(
|
|
214
|
+
console.error(`[crediball] ${errorLabel} failed:`, err);
|
|
232
215
|
}
|
|
233
216
|
return;
|
|
234
217
|
}
|
|
218
|
+
// Data-less mode with no handler: nothing to charge against.
|
|
235
219
|
setOpen(false);
|
|
236
220
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const { url } = await client.createSubscriptionCheckout({
|
|
255
|
-
planId: plan.id,
|
|
256
|
-
returnPath: currentReturnPath(),
|
|
257
|
-
});
|
|
258
|
-
if (typeof window !== "undefined")
|
|
259
|
-
window.location.assign(url);
|
|
260
|
-
}
|
|
261
|
-
catch (err) {
|
|
262
|
-
console.error("[crediball] subscription checkout failed:", err);
|
|
263
|
-
}
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
setOpen(false);
|
|
221
|
+
function handleAdd(amount) {
|
|
222
|
+
return runPaywallAction(onTopup, amount, () => startBuiltInCheckout({ eurAmount: amount }), "checkout");
|
|
223
|
+
}
|
|
224
|
+
function handleSelectPackage(pkg) {
|
|
225
|
+
return runPaywallAction(onSelectPackage, pkg, () => startBuiltInCheckout({ packageId: pkg.id }), "checkout");
|
|
226
|
+
}
|
|
227
|
+
function handleSelectPlan(plan) {
|
|
228
|
+
return runPaywallAction(onSelectPlan, plan, async () => {
|
|
229
|
+
if (!client)
|
|
230
|
+
return;
|
|
231
|
+
const { url } = await client.createSubscriptionCheckout({
|
|
232
|
+
planId: plan.id,
|
|
233
|
+
returnPath: currentReturnPath(),
|
|
234
|
+
});
|
|
235
|
+
if (typeof window !== "undefined")
|
|
236
|
+
window.location.assign(url);
|
|
237
|
+
}, "subscription checkout");
|
|
267
238
|
}
|
|
268
239
|
async function handleCancelSubscription(subscriptionId) {
|
|
269
240
|
await onCancelSubscription?.(subscriptionId);
|
|
@@ -281,8 +252,9 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
281
252
|
// layout box. Host props/CSS are overridden by design (dashboard wins).
|
|
282
253
|
const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
|
|
283
254
|
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
|
-
|
|
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 })] }));
|
|
256
|
+
const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
|
|
257
|
+
return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
|
|
286
258
|
}
|
|
287
259
|
/**
|
|
288
260
|
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Shared rendering shell for the Tier-2 "variable" components (CrediballBalance,
|
|
4
|
+
* CrediballUsage, CrediballLowCreditWarning): an inline <span> that inherits the
|
|
5
|
+
* surrounding font/color so it reads naturally wherever it's placed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function InlineValue({ className, style, children, }: {
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: CSSProperties;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Shared rendering shell for the Tier-2 "variable" components (CrediballBalance,
|
|
5
|
+
* CrediballUsage, CrediballLowCreditWarning): an inline <span> that inherits the
|
|
6
|
+
* surrounding font/color so it reads naturally wherever it's placed.
|
|
7
|
+
*/
|
|
8
|
+
export function InlineValue({ className, style, children, }) {
|
|
9
|
+
return (_jsx("span", { className: className, style: { font: "inherit", color: "currentColor", ...style }, children: children }));
|
|
10
|
+
}
|
package/dist/PaywallModal.d.ts
CHANGED
|
@@ -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;
|
package/dist/PaywallModal.js
CHANGED
|
@@ -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,
|
|
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",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type CrediballThemeOverrides } from "./theme";
|
|
3
|
+
export interface ReferralCardProps extends CrediballThemeOverrides {
|
|
4
|
+
/** Heading. Default "Invite friends". */
|
|
5
|
+
title?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Subtitle. Defaults to a reward blurb built from the dashboard config
|
|
8
|
+
* (e.g. "Earn 100 credits for every friend who joins.").
|
|
9
|
+
*/
|
|
10
|
+
description?: string;
|
|
11
|
+
/** Copy-button label. Default "Copy". */
|
|
12
|
+
copyLabel?: string;
|
|
13
|
+
/** Share-button label. Default "Share". */
|
|
14
|
+
shareLabel?: string;
|
|
15
|
+
/** Hide the clicks / converted counters row. */
|
|
16
|
+
hideStats?: boolean;
|
|
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
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Drop-in invite card built on useReferral(). Place anywhere inside
|
|
40
|
+
* <CrediballProvider publishableKey userId>:
|
|
41
|
+
*
|
|
42
|
+
* <ReferralCard />
|
|
43
|
+
*
|
|
44
|
+
* Shows the user's invite link with Copy/Share actions and their referral
|
|
45
|
+
* stats. Lazily creates the code on first render, renders nothing while
|
|
46
|
+
* loading or when the referral program is disabled in the dashboard, and is
|
|
47
|
+
* themed by the same `--crediball-*` variables as every other component.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ReferralCard({ title, description, copyLabel, shareLabel, hideStats, accentColor, style, demo, }: ReferralCardProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { theme, accentStyle } from "./theme";
|
|
4
|
+
import { formatCredits } from "./format";
|
|
5
|
+
import { useReferral } from "./useReferral";
|
|
6
|
+
/**
|
|
7
|
+
* Drop-in invite card built on useReferral(). Place anywhere inside
|
|
8
|
+
* <CrediballProvider publishableKey userId>:
|
|
9
|
+
*
|
|
10
|
+
* <ReferralCard />
|
|
11
|
+
*
|
|
12
|
+
* Shows the user's invite link with Copy/Share actions and their referral
|
|
13
|
+
* stats. Lazily creates the code on first render, renders nothing while
|
|
14
|
+
* loading or when the referral program is disabled in the dashboard, and is
|
|
15
|
+
* themed by the same `--crediball-*` variables as every other component.
|
|
16
|
+
*/
|
|
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;
|
|
32
|
+
if (loading || !enabled || !code)
|
|
33
|
+
return null;
|
|
34
|
+
const resolvedDescription = description ??
|
|
35
|
+
(rewardCredits > 0
|
|
36
|
+
? referredRewardCredits > 0
|
|
37
|
+
? `Earn ${formatCredits(rewardCredits)} credits for every friend who joins — they get ${formatCredits(referredRewardCredits)} credits too.`
|
|
38
|
+
: `Earn ${formatCredits(rewardCredits)} credits for every friend who joins.`
|
|
39
|
+
: undefined);
|
|
40
|
+
const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function";
|
|
41
|
+
const buttonBase = {
|
|
42
|
+
appearance: "none",
|
|
43
|
+
cursor: "pointer",
|
|
44
|
+
fontFamily: theme.font,
|
|
45
|
+
fontSize: 14,
|
|
46
|
+
padding: "8px 16px",
|
|
47
|
+
borderRadius: theme.radiusPill,
|
|
48
|
+
whiteSpace: "nowrap",
|
|
49
|
+
};
|
|
50
|
+
return (_jsxs("div", { style: {
|
|
51
|
+
display: "flex",
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
gap: 14,
|
|
54
|
+
padding: 24,
|
|
55
|
+
border: `1px solid ${theme.hairline}`,
|
|
56
|
+
borderRadius: theme.radiusCard,
|
|
57
|
+
background: theme.canvas,
|
|
58
|
+
fontFamily: theme.font,
|
|
59
|
+
...accentStyle(accentColor),
|
|
60
|
+
...style,
|
|
61
|
+
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 16, fontWeight: 600, color: theme.ink }, children: title }), resolvedDescription ? (_jsx("div", { style: { fontSize: 13, color: theme.inkMuted, marginTop: 2 }, children: resolvedDescription })) : null] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }, children: [_jsx("div", { style: {
|
|
62
|
+
flex: "1 1 180px",
|
|
63
|
+
minWidth: 0,
|
|
64
|
+
overflow: "hidden",
|
|
65
|
+
textOverflow: "ellipsis",
|
|
66
|
+
whiteSpace: "nowrap",
|
|
67
|
+
fontSize: 14,
|
|
68
|
+
color: theme.ink,
|
|
69
|
+
border: `1px solid ${theme.hairline}`,
|
|
70
|
+
borderRadius: theme.radiusPill,
|
|
71
|
+
padding: "8px 14px",
|
|
72
|
+
}, title: link ?? code, children: link ?? code }), _jsx("button", { onClick: () => void copy(), style: {
|
|
73
|
+
...buttonBase,
|
|
74
|
+
border: `1px solid ${theme.hairline}`,
|
|
75
|
+
background: "transparent",
|
|
76
|
+
color: theme.ink,
|
|
77
|
+
}, children: copied ? "Copied!" : copyLabel }), canShare ? (_jsx("button", { onClick: () => void share(), style: {
|
|
78
|
+
...buttonBase,
|
|
79
|
+
border: "none",
|
|
80
|
+
background: theme.accent,
|
|
81
|
+
color: theme.onAccent,
|
|
82
|
+
}, children: shareLabel })) : null] }), hideStats ? null : (_jsxs("div", { style: { display: "flex", gap: 16, fontSize: 13, color: theme.inkMuted }, children: [_jsxs("span", { children: [_jsx("strong", { style: { color: theme.ink, fontWeight: 600 }, children: clicks }), " clicks"] }), _jsxs("span", { children: [_jsx("strong", { style: { color: theme.ink, fontWeight: 600 }, children: rewards.converted }), " ", "joined"] }), rewards.creditsEarned > 0 ? (_jsxs("span", { children: [_jsx("strong", { style: { color: theme.ink, fontWeight: 600 }, children: formatCredits(rewards.creditsEarned) }), " ", "credits earned"] })) : null] }))] }));
|
|
83
|
+
}
|
package/dist/format.d.ts
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function formatCredits(credits: number): string;
|
|
3
|
-
/** Formats a monetary amount in the given ISO 4217 currency, falling back to a plain number + code for unknown currencies. */
|
|
4
|
-
export declare function formatMoney(amount: number, currency: string): string;
|
|
1
|
+
export { formatCredits, formatMoney } from "@crediball/core";
|
package/dist/format.js
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export function formatCredits(credits) {
|
|
3
|
-
return new Intl.NumberFormat(undefined).format(Math.round(credits));
|
|
4
|
-
}
|
|
5
|
-
/** Formats a monetary amount in the given ISO 4217 currency, falling back to a plain number + code for unknown currencies. */
|
|
6
|
-
export function formatMoney(amount, currency) {
|
|
7
|
-
try {
|
|
8
|
-
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(amount);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return `${amount.toFixed(2)} ${currency}`;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
export { formatCredits, formatMoney } from "@crediball/core";
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export { PaywallModal, type PaywallModalProps, type PackageOption, type Subscrip
|
|
|
3
3
|
export { CreditIndicator, type CreditIndicatorProps } from "./CreditIndicator";
|
|
4
4
|
export { CrediballProvider, useCrediball, useCrediballOptional, type CrediballProviderProps, } from "./CrediballProvider";
|
|
5
5
|
export { usePaywall, isInsufficientCreditsError, type UsePaywallProps, type UsePaywallResult, } from "./usePaywall";
|
|
6
|
+
export { useReferral, type UseReferralResult } from "./useReferral";
|
|
7
|
+
export { ReferralCard, type ReferralCardProps } from "./ReferralCard";
|
|
6
8
|
export { tokens, theme, cssVars, accentStyle, type CrediballThemeOverrides } from "./theme";
|
|
7
9
|
export { notifyInsufficientCredits } from "./creditEvent";
|
|
8
10
|
export { CrediballBalance, type CrediballBalanceProps } from "./variables/CrediballBalance";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export { PaywallModal, } from "./PaywallModal";
|
|
|
5
5
|
export { CreditIndicator } from "./CreditIndicator";
|
|
6
6
|
export { CrediballProvider, useCrediball, useCrediballOptional, } from "./CrediballProvider";
|
|
7
7
|
export { usePaywall, isInsufficientCreditsError, } from "./usePaywall";
|
|
8
|
+
export { useReferral } from "./useReferral";
|
|
9
|
+
export { ReferralCard } from "./ReferralCard";
|
|
8
10
|
export { tokens, theme, cssVars, accentStyle } from "./theme";
|
|
9
11
|
export { notifyInsufficientCredits } from "./creditEvent";
|
|
10
12
|
// Tier 2 — "variables" you place anywhere inside <CrediballProvider>.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface UseReferralResult {
|
|
2
|
+
/** False while loading, or when the referral program is off in the dashboard. */
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
/** The user's referral code (lazily created server-side on first read). */
|
|
5
|
+
code: string | null;
|
|
6
|
+
/** Full invite link: the dashboard's link template, or `origin/?ref=CODE` when unset. */
|
|
7
|
+
link: string | null;
|
|
8
|
+
/** How many times the invite link was opened. */
|
|
9
|
+
clicks: number;
|
|
10
|
+
/** Referral outcomes: pending / converted counts and credits earned so far. */
|
|
11
|
+
rewards: {
|
|
12
|
+
pending: number;
|
|
13
|
+
converted: number;
|
|
14
|
+
creditsEarned: number;
|
|
15
|
+
};
|
|
16
|
+
/** Credits the referrer earns per converted referral (dashboard config). */
|
|
17
|
+
rewardCredits: number;
|
|
18
|
+
/** Credits the invited friend gets on conversion (dashboard config, 0 = off). */
|
|
19
|
+
referredRewardCredits: number;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
/** Copy the invite link (falls back to the code) to the clipboard. */
|
|
23
|
+
copy: () => Promise<void>;
|
|
24
|
+
/** True for 2s after a successful copy — for "Copied!" feedback. */
|
|
25
|
+
copied: boolean;
|
|
26
|
+
/** Open the native share sheet with the invite link; falls back to copy. */
|
|
27
|
+
share: () => Promise<void>;
|
|
28
|
+
/** Refetch code/stats. */
|
|
29
|
+
refresh: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Headless referral hook — render any invite UI on top of it:
|
|
33
|
+
*
|
|
34
|
+
* const { link, copy, share, rewards, loading } = useReferral();
|
|
35
|
+
*
|
|
36
|
+
* Must be used inside <CrediballProvider publishableKey userId>. The referral
|
|
37
|
+
* code is created lazily by the server on the first call and stays stable.
|
|
38
|
+
* When the dashboard's referral program is disabled, `enabled` is false and
|
|
39
|
+
* everything else stays empty — render nothing in that case (as
|
|
40
|
+
* <ReferralCard/> does).
|
|
41
|
+
*/
|
|
42
|
+
export declare function useReferral(): UseReferralResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { fetchReferral } from "@crediball/core";
|
|
4
|
+
import { useCrediballConfig } from "./CrediballProvider";
|
|
5
|
+
const EMPTY_REWARDS = { pending: 0, converted: 0, creditsEarned: 0 };
|
|
6
|
+
/**
|
|
7
|
+
* Headless referral hook — render any invite UI on top of it:
|
|
8
|
+
*
|
|
9
|
+
* const { link, copy, share, rewards, loading } = useReferral();
|
|
10
|
+
*
|
|
11
|
+
* Must be used inside <CrediballProvider publishableKey userId>. The referral
|
|
12
|
+
* code is created lazily by the server on the first call and stays stable.
|
|
13
|
+
* When the dashboard's referral program is disabled, `enabled` is false and
|
|
14
|
+
* everything else stays empty — render nothing in that case (as
|
|
15
|
+
* <ReferralCard/> does).
|
|
16
|
+
*/
|
|
17
|
+
export function useReferral() {
|
|
18
|
+
const { publishableKey, userId, apiUrl } = useCrediballConfig();
|
|
19
|
+
const [data, setData] = useState(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const [copied, setCopied] = useState(false);
|
|
23
|
+
const copiedTimer = useRef(null);
|
|
24
|
+
const refresh = useCallback(async () => {
|
|
25
|
+
if (!publishableKey || !userId) {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const next = await fetchReferral({ publishableKey, userId, apiUrl });
|
|
31
|
+
setData(next);
|
|
32
|
+
setError(null);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}, [publishableKey, userId, apiUrl]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
void refresh();
|
|
44
|
+
}, [refresh]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return () => {
|
|
47
|
+
if (copiedTimer.current)
|
|
48
|
+
clearTimeout(copiedTimer.current);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
const link = data?.link ?? null;
|
|
52
|
+
const code = data?.code ?? null;
|
|
53
|
+
const copy = useCallback(async () => {
|
|
54
|
+
const text = link ?? code;
|
|
55
|
+
if (!text || typeof navigator === "undefined" || !navigator.clipboard)
|
|
56
|
+
return;
|
|
57
|
+
await navigator.clipboard.writeText(text);
|
|
58
|
+
setCopied(true);
|
|
59
|
+
if (copiedTimer.current)
|
|
60
|
+
clearTimeout(copiedTimer.current);
|
|
61
|
+
copiedTimer.current = setTimeout(() => setCopied(false), 2000);
|
|
62
|
+
}, [link, code]);
|
|
63
|
+
const share = useCallback(async () => {
|
|
64
|
+
const url = link ?? undefined;
|
|
65
|
+
const nav = typeof navigator === "undefined" ? undefined : navigator;
|
|
66
|
+
if (url && nav?.share) {
|
|
67
|
+
try {
|
|
68
|
+
await nav.share({ url });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// dismissed or unsupported payload — fall through to copy
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
await copy();
|
|
76
|
+
}, [link, copy]);
|
|
77
|
+
return {
|
|
78
|
+
enabled: data?.enabled ?? false,
|
|
79
|
+
code,
|
|
80
|
+
link,
|
|
81
|
+
clicks: data?.clicks ?? 0,
|
|
82
|
+
rewards: data?.rewards ?? EMPTY_REWARDS,
|
|
83
|
+
rewardCredits: data?.rewardCredits ?? 0,
|
|
84
|
+
referredRewardCredits: data?.referredRewardCredits ?? 0,
|
|
85
|
+
loading,
|
|
86
|
+
error,
|
|
87
|
+
copy,
|
|
88
|
+
copied,
|
|
89
|
+
share,
|
|
90
|
+
refresh,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useCrediball } from "../CrediballProvider";
|
|
4
4
|
import { formatCredits } from "../format";
|
|
5
|
+
import { InlineValue } from "../InlineValue";
|
|
5
6
|
/**
|
|
6
7
|
* Tier 2 — a "variable" you drop anywhere inside <CrediballProvider>. Renders
|
|
7
8
|
* as an inline <span> that inherits the surrounding font, size, and color
|
|
@@ -15,5 +16,5 @@ export function CrediballBalance({ className, style, format = formatCredits, chi
|
|
|
15
16
|
if (typeof children === "function") {
|
|
16
17
|
return _jsx(_Fragment, { children: children({ balance, loading }) });
|
|
17
18
|
}
|
|
18
|
-
return (_jsx(
|
|
19
|
+
return (_jsx(InlineValue, { className: className, style: style, children: children ?? (balance == null ? "—" : format(balance)) }));
|
|
19
20
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { isBalanceLow } from "@crediball/core";
|
|
3
4
|
import { useCrediball } from "../CrediballProvider";
|
|
5
|
+
import { InlineValue } from "../InlineValue";
|
|
4
6
|
/**
|
|
5
7
|
* Tier 2 — renders nothing until the user's balance is low, then shows a
|
|
6
8
|
* message inline. Place it next to a specific action ("this costs 20 credits")
|
|
@@ -12,12 +14,12 @@ export function CrediballLowCreditWarning({ className, style, threshold, message
|
|
|
12
14
|
const { balance, isLow: contextIsLow, content } = useCrediball();
|
|
13
15
|
// Prefer the host prop, then the dashboard-published default, then the provider's.
|
|
14
16
|
const effectiveThreshold = threshold ?? content?.lowCreditThreshold;
|
|
15
|
-
const isLow =
|
|
17
|
+
const isLow = isBalanceLow(balance, effectiveThreshold, contextIsLow);
|
|
16
18
|
if (!isLow)
|
|
17
19
|
return null;
|
|
18
20
|
if (typeof children === "function") {
|
|
19
21
|
return _jsx(_Fragment, { children: children({ balance, isLow }) });
|
|
20
22
|
}
|
|
21
23
|
const resolvedMessage = message ?? content?.lowCreditMessage ?? "Low credit balance — top up to keep going.";
|
|
22
|
-
return (_jsx(
|
|
24
|
+
return (_jsx(InlineValue, { className: className, style: style, children: children ?? resolvedMessage }));
|
|
23
25
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useCrediball } from "../CrediballProvider";
|
|
4
4
|
import { formatCredits } from "../format";
|
|
5
|
+
import { InlineValue } from "../InlineValue";
|
|
5
6
|
/**
|
|
6
7
|
* Tier 2 — credits consumed in the user's current period (their subscription's
|
|
7
8
|
* billing cycle, or the calendar month for non-subscribers). Inline, inherits
|
|
@@ -15,5 +16,5 @@ export function CrediballUsage({ className, style, show = "used", format = forma
|
|
|
15
16
|
if (typeof children === "function") {
|
|
16
17
|
return _jsx(_Fragment, { children: children({ usage, value, loading }) });
|
|
17
18
|
}
|
|
18
|
-
return (_jsx(
|
|
19
|
+
return (_jsx(InlineValue, { className: className, style: style, children: children ?? (value == null ? "—" : format(value)) }));
|
|
19
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediball/react",
|
|
3
|
-
"version": "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>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react": ">=18"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@crediball/core": "^0.
|
|
54
|
+
"@crediball/core": "^0.4.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "^18.3.11",
|