@crediball/react 0.3.1 → 0.3.2
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/PaywallModal.d.ts +38 -1
- package/dist/PaywallModal.js +66 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/usePaywall.d.ts +5 -3
- package/dist/usePaywall.js +1 -1
- package/package.json +1 -1
package/dist/PaywallModal.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface PackageOption {
|
|
|
7
7
|
price: number;
|
|
8
8
|
credits: number;
|
|
9
9
|
}
|
|
10
|
+
/** A recurring subscription plan (returned by listPackages()). */
|
|
11
|
+
export interface SubscriptionPlanOption {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
price: number;
|
|
15
|
+
credits: number;
|
|
16
|
+
period: "monthly" | "yearly";
|
|
17
|
+
}
|
|
18
|
+
/** The end user's current active subscription (returned by listPackages() with userId). */
|
|
19
|
+
export interface ActiveSubscriptionInfo {
|
|
20
|
+
subscriptionId: string;
|
|
21
|
+
planName: string;
|
|
22
|
+
planPeriod: string;
|
|
23
|
+
currentPeriodEnd: string;
|
|
24
|
+
}
|
|
10
25
|
export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
11
26
|
open: boolean;
|
|
12
27
|
/**
|
|
@@ -21,6 +36,28 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
21
36
|
* calculation needed; Crediball derives it from the package.
|
|
22
37
|
*/
|
|
23
38
|
onSelectPackage?: (pkg: PackageOption) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Subscription plans from `meter.listPackages()`. When provided (and the user
|
|
41
|
+
* has no active subscription), the modal shows a "Subscribe" section above
|
|
42
|
+
* one-time packages.
|
|
43
|
+
*/
|
|
44
|
+
subscriptionPlans?: SubscriptionPlanOption[];
|
|
45
|
+
/**
|
|
46
|
+
* Called when the user selects a subscription plan. Call
|
|
47
|
+
* `meter.subscribe({ userId, planId: plan.id })` in your backend.
|
|
48
|
+
*/
|
|
49
|
+
onSelectPlan?: (plan: SubscriptionPlanOption) => void;
|
|
50
|
+
/**
|
|
51
|
+
* The end user's active subscription (from `meter.listPackages({ userId })`).
|
|
52
|
+
* When provided, the subscription section is replaced by a subscription-status
|
|
53
|
+
* banner and a cancel option.
|
|
54
|
+
*/
|
|
55
|
+
activeSubscription?: ActiveSubscriptionInfo | null;
|
|
56
|
+
/**
|
|
57
|
+
* Called when the user clicks "Cancel subscription" in the paywall.
|
|
58
|
+
* Call `meter.cancelSubscription({ userId, subscriptionId })` in your backend.
|
|
59
|
+
*/
|
|
60
|
+
onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
|
|
24
61
|
/**
|
|
25
62
|
* Legacy: flat list of euro amounts. Prefer `packages` + `onSelectPackage`.
|
|
26
63
|
* When both are provided, `packages` takes precedence.
|
|
@@ -52,4 +89,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
52
89
|
currency?: string;
|
|
53
90
|
style?: CSSProperties;
|
|
54
91
|
}
|
|
55
|
-
export declare function PaywallModal({ open, packages, onSelectPackage, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, customMin, customMax, currencySymbol, balance, balanceRate, currency, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
|
92
|
+
export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, customMin, customMax, currencySymbol, balance, balanceRate, currency, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
package/dist/PaywallModal.js
CHANGED
|
@@ -17,11 +17,13 @@ function fmtMoney(n, currency) {
|
|
|
17
17
|
return `${n.toFixed(2)} ${currency}`;
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
export function PaywallModal({ open, packages, onSelectPackage, amounts = [5, 10], amountPrefix = "Add €", title = "You need more credits to continue.", description = "Top up to keep using this feature.", onAdd, onClose, allowCustom = false, customMin = 1, customMax, currencySymbol = "€", balance, balanceRate, currency = "EUR", accentColor = tokens.primary, style, }) {
|
|
20
|
+
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, customMin = 1, customMax, currencySymbol = "€", balance, balanceRate, currency = "EUR", accentColor = tokens.primary, style, }) {
|
|
21
21
|
// Stack the top-up buttons one-per-line on narrow (mobile) screens.
|
|
22
22
|
const [narrow, setNarrow] = useState(false);
|
|
23
23
|
const [customValue, setCustomValue] = useState("");
|
|
24
24
|
const [customError, setCustomError] = useState(null);
|
|
25
|
+
const [cancelPending, setCancelPending] = useState(false);
|
|
26
|
+
const [cancelDone, setCancelDone] = useState(false);
|
|
25
27
|
useEffect(() => {
|
|
26
28
|
if (typeof window === "undefined" || !window.matchMedia)
|
|
27
29
|
return;
|
|
@@ -33,6 +35,18 @@ export function PaywallModal({ open, packages, onSelectPackage, amounts = [5, 10
|
|
|
33
35
|
}, []);
|
|
34
36
|
if (!open)
|
|
35
37
|
return null;
|
|
38
|
+
async function handleCancelSubscription() {
|
|
39
|
+
if (!activeSubscription || cancelPending)
|
|
40
|
+
return;
|
|
41
|
+
setCancelPending(true);
|
|
42
|
+
try {
|
|
43
|
+
await onCancelSubscription?.(activeSubscription.subscriptionId);
|
|
44
|
+
setCancelDone(true);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setCancelPending(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
36
50
|
function submitCustom() {
|
|
37
51
|
const amt = Number(customValue);
|
|
38
52
|
if (!Number.isFinite(amt) || amt <= 0) {
|
|
@@ -90,14 +104,57 @@ export function PaywallModal({ open, packages, onSelectPackage, amounts = [5, 10
|
|
|
90
104
|
textTransform: "uppercase",
|
|
91
105
|
color: tokens.inkMuted,
|
|
92
106
|
marginBottom: 4,
|
|
93
|
-
}, children: "Your balance" }), _jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }, children: [_jsxs("span", { style: { fontSize: 22, fontWeight: 600, color: tokens.ink }, children: [fmtCredits(balance), " credits"] }), balanceRate && balanceRate > 0 ? (_jsxs("span", { style: { fontSize: 13, color: tokens.inkMuted }, children: ["\u2248 ", fmtMoney(balance / balanceRate, currency)] })) : null] })] })), _jsx("h3", { style: { margin: 0, fontSize: 24, fontWeight: 600, color: tokens.ink, letterSpacing: "-0.01em" }, children:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
:
|
|
107
|
+
}, children: "Your balance" }), _jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }, children: [_jsxs("span", { style: { fontSize: 22, fontWeight: 600, color: tokens.ink }, children: [fmtCredits(balance), " credits"] }), balanceRate && balanceRate > 0 ? (_jsxs("span", { style: { fontSize: 13, color: tokens.inkMuted }, children: ["\u2248 ", fmtMoney(balance / balanceRate, currency)] })) : null] })] })), _jsx("h3", { style: { margin: 0, fontSize: 24, fontWeight: 600, color: tokens.ink, letterSpacing: "-0.01em" }, children: activeSubscription
|
|
108
|
+
? "You've used all your credits for this period."
|
|
109
|
+
: title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize: 14, color: tokens.inkMuted }, children: activeSubscription
|
|
110
|
+
? `Your ${activeSubscription.planName} credits renew on ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()}.`
|
|
111
|
+
: description }), activeSubscription && (_jsxs("div", { style: {
|
|
112
|
+
marginTop: 16,
|
|
113
|
+
padding: "12px 16px",
|
|
114
|
+
borderRadius: tokens.radiusCard,
|
|
115
|
+
background: "#f5f5f7",
|
|
116
|
+
border: `1px solid ${tokens.hairline}`,
|
|
117
|
+
}, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 600, color: tokens.ink }, children: cancelDone ? "Subscription canceled" : `${activeSubscription.planName} · ${activeSubscription.planPeriod}` }), !cancelDone && (_jsx("button", { onClick: handleCancelSubscription, disabled: cancelPending, style: {
|
|
118
|
+
marginTop: 8,
|
|
119
|
+
appearance: "none",
|
|
120
|
+
border: "none",
|
|
121
|
+
background: "transparent",
|
|
122
|
+
cursor: cancelPending ? "default" : "pointer",
|
|
123
|
+
padding: 0,
|
|
124
|
+
fontSize: 13,
|
|
125
|
+
color: tokens.inkMuted,
|
|
126
|
+
fontFamily: tokens.fontStack,
|
|
127
|
+
textDecoration: "underline",
|
|
128
|
+
}, children: cancelPending ? "Canceling…" : "Cancel subscription" }))] })), !activeSubscription && subscriptionPlans && subscriptionPlans.length > 0 && (_jsxs("div", { style: { marginTop: 24 }, children: [_jsx("div", { style: {
|
|
129
|
+
fontSize: 11,
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
letterSpacing: "0.06em",
|
|
132
|
+
textTransform: "uppercase",
|
|
133
|
+
color: tokens.inkMuted,
|
|
134
|
+
marginBottom: 8,
|
|
135
|
+
}, children: "Subscribe" }), _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: subscriptionPlans.map((plan) => (_jsxs("button", { onClick: () => onSelectPlan?.(plan), style: {
|
|
136
|
+
...buttonStyle,
|
|
137
|
+
flex: "none",
|
|
138
|
+
display: "flex",
|
|
139
|
+
justifyContent: "space-between",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
}, children: [_jsx("span", { children: plan.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [fmtMoney(plan.price, currency), "/", plan.period === "monthly" ? "mo" : "yr", " \u00B7 ", fmtCredits(plan.credits), " cr"] })] }, plan.id))) })] })), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10, marginTop: 24 }, children: [(!activeSubscription && subscriptionPlans && subscriptionPlans.length > 0) && (_jsx("div", { style: {
|
|
142
|
+
fontSize: 11,
|
|
143
|
+
fontWeight: 600,
|
|
144
|
+
letterSpacing: "0.06em",
|
|
145
|
+
textTransform: "uppercase",
|
|
146
|
+
color: tokens.inkMuted,
|
|
147
|
+
marginBottom: -2,
|
|
148
|
+
}, children: activeSubscription ? "Add extra credits" : "Or buy credits" })), activeSubscription && packages && packages.length > 0 && (_jsx("div", { style: {
|
|
149
|
+
fontSize: 11,
|
|
150
|
+
fontWeight: 600,
|
|
151
|
+
letterSpacing: "0.06em",
|
|
152
|
+
textTransform: "uppercase",
|
|
153
|
+
color: tokens.inkMuted,
|
|
154
|
+
marginBottom: -2,
|
|
155
|
+
}, children: "Add extra credits" })), packages && packages.length > 0
|
|
156
|
+
? packages.map((pkg) => (_jsxs("button", { onClick: () => onSelectPackage?.(pkg), style: { ...buttonStyle, flex: "none", display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [_jsx("span", { children: pkg.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [fmtMoney(pkg.price, currency), " \u00B7 ", fmtCredits(pkg.credits), " cr"] })] }, pkg.id)))
|
|
157
|
+
: (!activeSubscription && (_jsx("div", { style: { display: "flex", flexDirection: narrow ? "column" : "row", gap: 12 }, children: amounts.map((amount) => (_jsxs("button", { onClick: () => onAdd?.(amount), style: buttonStyle, children: [amountPrefix, amount] }, amount))) })))] }), allowCustom ? (_jsxs("div", { style: { marginTop: 12 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("span", { style: { fontSize: 17, color: tokens.inkMuted }, children: currencySymbol }), _jsx("input", { value: customValue, onChange: (e) => setCustomValue(e.target.value), inputMode: "decimal", placeholder: String(customMin), style: {
|
|
101
158
|
flex: 1,
|
|
102
159
|
minWidth: 0,
|
|
103
160
|
appearance: "none",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { CreditsBadge, type CreditsBadgeProps } from "./CreditsBadge";
|
|
2
|
-
export { PaywallModal, type PaywallModalProps, type PackageOption } from "./PaywallModal";
|
|
2
|
+
export { PaywallModal, type PaywallModalProps, type PackageOption, type SubscriptionPlanOption, type ActiveSubscriptionInfo, } from "./PaywallModal";
|
|
3
3
|
export { CreditIndicator, type CreditIndicatorProps } from "./CreditIndicator";
|
|
4
4
|
export { CrediballProvider, useCrediball, type CrediballProviderProps, } from "./CrediballProvider";
|
|
5
5
|
export { usePaywall, type UsePaywallProps, type UsePaywallResult, } from "./usePaywall";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @crediball/react — drop-in, self-contained components for showing credits
|
|
2
2
|
// inside a host AI app. No Crediball branding, no end-user login, no Tailwind.
|
|
3
3
|
export { CreditsBadge } from "./CreditsBadge";
|
|
4
|
-
export { PaywallModal } from "./PaywallModal";
|
|
4
|
+
export { PaywallModal, } from "./PaywallModal";
|
|
5
5
|
export { CreditIndicator } from "./CreditIndicator";
|
|
6
6
|
export { CrediballProvider, useCrediball, } from "./CrediballProvider";
|
|
7
7
|
export { usePaywall, } from "./usePaywall";
|
package/dist/usePaywall.d.ts
CHANGED
|
@@ -7,10 +7,12 @@ export type UsePaywallProps = Pick<PaywallModalProps, "amounts" | "amountPrefix"
|
|
|
7
7
|
onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
|
|
8
8
|
/**
|
|
9
9
|
* Auto-detect HTTP 402 `insufficient_credits` responses from any fetch() call
|
|
10
|
-
* and open the paywall automatically. Defaults to
|
|
10
|
+
* and open the paywall automatically. Defaults to false.
|
|
11
11
|
*
|
|
12
|
-
* Set to
|
|
13
|
-
*
|
|
12
|
+
* Set to true only if you are NOT using <CrediballProvider> and want automatic
|
|
13
|
+
* interception. If your app already wraps with <CrediballProvider watchFetch>
|
|
14
|
+
* (the recommended pattern), leave this false — double-patching is harmless
|
|
15
|
+
* but unnecessary.
|
|
14
16
|
*
|
|
15
17
|
* Note: SSE/streaming routes surface credit errors as stream events (HTTP 200),
|
|
16
18
|
* not HTTP 402 — call trigger() manually from your stream error handler.
|
package/dist/usePaywall.js
CHANGED
|
@@ -28,7 +28,7 @@ import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
|
28
28
|
*
|
|
29
29
|
* return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
|
|
30
30
|
*/
|
|
31
|
-
export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch =
|
|
31
|
+
export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch = false, } = {}) {
|
|
32
32
|
const [isOpen, setIsOpen] = useState(false);
|
|
33
33
|
const trigger = useCallback(() => setIsOpen(true), []);
|
|
34
34
|
const dismiss = useCallback(() => setIsOpen(false), []);
|
package/package.json
CHANGED