@crediball/react 0.3.6 → 0.4.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 +67 -8
- package/dist/CrediballProvider.js +99 -10
- package/dist/CreditIndicator.d.ts +9 -4
- package/dist/CreditIndicator.js +16 -10
- package/dist/CreditsBadge.d.ts +6 -3
- package/dist/CreditsBadge.js +17 -18
- package/dist/PaywallModal.d.ts +17 -3
- package/dist/PaywallModal.js +85 -89
- package/dist/creditEvent.d.ts +6 -19
- package/dist/creditEvent.js +6 -29
- package/dist/fetchWatcher.d.ts +15 -9
- package/dist/fetchWatcher.js +29 -37
- package/dist/format.d.ts +4 -0
- package/dist/format.js +13 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +7 -2
- package/dist/theme.d.ts +39 -0
- package/dist/theme.js +43 -0
- package/dist/variables/CrediballBalance.d.ts +26 -0
- package/dist/variables/CrediballBalance.js +19 -0
- package/dist/variables/CrediballLowCreditWarning.d.ts +25 -0
- package/dist/variables/CrediballLowCreditWarning.js +20 -0
- package/dist/variables/CrediballTopUpButton.d.ts +18 -0
- package/dist/variables/CrediballTopUpButton.js +27 -0
- package/dist/variables/CrediballUsage.d.ts +24 -0
- package/dist/variables/CrediballUsage.js +19 -0
- package/package.json +4 -1
package/dist/PaywallModal.js
CHANGED
|
@@ -1,37 +1,46 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import {
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { theme, accentStyle } from "./theme";
|
|
5
|
+
import { formatCredits, formatMoney } from "./format";
|
|
6
|
+
/** Shared uppercase eyebrow-label style used above the subscribe/buy-credits sections. */
|
|
7
|
+
function sectionLabelStyle(marginBottom) {
|
|
8
|
+
return {
|
|
9
|
+
fontSize: 11,
|
|
10
|
+
fontWeight: 600,
|
|
11
|
+
letterSpacing: "0.06em",
|
|
12
|
+
textTransform: "uppercase",
|
|
13
|
+
color: theme.inkMuted,
|
|
14
|
+
marginBottom,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Pattern B — Paywall moment. Render when a user has insufficient credits to
|
|
7
|
-
* continue. The host controls when it appears;
|
|
19
|
+
* continue. The host controls when it appears; shows a small "Powered by
|
|
20
|
+
* Crediball" line by default (set `hideBranding` to remove it).
|
|
8
21
|
*/
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return `${n.toFixed(2)} ${currency}`;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
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
|
-
// Stack the top-up buttons one-per-line on narrow (mobile) screens.
|
|
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, }) {
|
|
23
|
+
// Stack the top-up buttons one-per-line when the dialog itself is narrow.
|
|
24
|
+
// Measured off the dialog's own box (via ResizeObserver) rather than the
|
|
25
|
+
// window — in normal use the two are the same width, but this also makes
|
|
26
|
+
// the layout correctly respond when the dialog is rendered inside a
|
|
27
|
+
// narrower container (e.g. a device-mockup preview) on a wide screen.
|
|
22
28
|
const [narrow, setNarrow] = useState(false);
|
|
29
|
+
const overlayRef = useRef(null);
|
|
23
30
|
const [customValue, setCustomValue] = useState("");
|
|
24
31
|
const [customError, setCustomError] = useState(null);
|
|
25
32
|
const [cancelPending, setCancelPending] = useState(false);
|
|
26
33
|
const [cancelDone, setCancelDone] = useState(false);
|
|
27
34
|
useEffect(() => {
|
|
28
|
-
|
|
35
|
+
const el = overlayRef.current;
|
|
36
|
+
if (!el || typeof ResizeObserver === "undefined")
|
|
29
37
|
return;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const observer = new ResizeObserver((entries) => {
|
|
39
|
+
const width = entries[0]?.contentRect.width ?? el.clientWidth;
|
|
40
|
+
setNarrow(width <= 480);
|
|
41
|
+
});
|
|
42
|
+
observer.observe(el);
|
|
43
|
+
return () => observer.disconnect();
|
|
35
44
|
}, []);
|
|
36
45
|
if (!open)
|
|
37
46
|
return null;
|
|
@@ -69,14 +78,16 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
69
78
|
appearance: "none",
|
|
70
79
|
border: "none",
|
|
71
80
|
cursor: "pointer",
|
|
72
|
-
background:
|
|
73
|
-
color:
|
|
74
|
-
fontFamily:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
background: theme.accent,
|
|
82
|
+
color: theme.onAccent,
|
|
83
|
+
fontFamily: theme.font,
|
|
84
|
+
fontWeight: 500,
|
|
85
|
+
fontSize: 14,
|
|
86
|
+
lineHeight: "24px",
|
|
87
|
+
padding: "8px 16px",
|
|
88
|
+
borderRadius: theme.radiusPill,
|
|
78
89
|
};
|
|
79
|
-
return (_jsx("div", { role: "dialog", "aria-modal": "true", style: {
|
|
90
|
+
return (_jsx("div", { ref: overlayRef, role: "dialog", "aria-modal": "true", style: {
|
|
80
91
|
position: "fixed",
|
|
81
92
|
inset: 0,
|
|
82
93
|
zIndex: 2147483647,
|
|
@@ -84,37 +95,42 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
84
95
|
alignItems: "center",
|
|
85
96
|
justifyContent: "center",
|
|
86
97
|
padding: 24,
|
|
87
|
-
background:
|
|
88
|
-
fontFamily:
|
|
98
|
+
background: theme.overlay,
|
|
99
|
+
fontFamily: theme.font,
|
|
100
|
+
// Mobile Safari/Chrome auto-inflate font sizes in narrow columns for
|
|
101
|
+
// readability, which throws off this dialog's fixed layout (extra
|
|
102
|
+
// text wrapping eats the padding around it). Opt out — sizes here
|
|
103
|
+
// are already chosen deliberately.
|
|
104
|
+
WebkitTextSizeAdjust: "100%",
|
|
105
|
+
textSizeAdjust: "100%",
|
|
106
|
+
...accentStyle(accentColor),
|
|
89
107
|
}, onClick: onClose, children: _jsxs("div", { onClick: (e) => e.stopPropagation(), style: {
|
|
90
108
|
width: "100%",
|
|
91
109
|
maxWidth: 420,
|
|
92
110
|
padding: 32,
|
|
93
|
-
borderRadius:
|
|
94
|
-
background:
|
|
111
|
+
borderRadius: theme.radiusCard,
|
|
112
|
+
background: theme.canvas,
|
|
113
|
+
boxShadow: "0px 4px 6px 0px rgba(0, 0, 0, 0.09)",
|
|
95
114
|
...style,
|
|
96
115
|
}, children: [balance != null && (_jsxs("div", { style: {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
color: tokens.inkMuted,
|
|
106
|
-
marginBottom: 4,
|
|
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
|
|
116
|
+
display: "flex",
|
|
117
|
+
gap: 16,
|
|
118
|
+
marginBottom: 16,
|
|
119
|
+
paddingBottom: 16,
|
|
120
|
+
borderBottom: `1px solid ${theme.hairline}`,
|
|
121
|
+
}, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: [_jsx("span", { style: { fontSize: 11, color: theme.inkMuted }, children: "Balance" }), _jsxs("span", { style: { fontSize: 12, fontWeight: 500, color: theme.ink }, children: [formatCredits(balance), balanceRate && balanceRate > 0
|
|
122
|
+
? ` (≈${formatMoney(balance / balanceRate, currency)})`
|
|
123
|
+
: ""] })] }), usage != null ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 2 }, children: [_jsx("span", { style: { fontSize: 11, color: theme.inkMuted }, children: usageLabel }), _jsxs("span", { style: { fontSize: 12, fontWeight: 500, color: theme.ink }, children: [formatCredits(usage), " credits"] })] })) : null] })), _jsx("h3", { style: { margin: 0, fontSize: 18, fontWeight: 600, lineHeight: "28px", color: theme.ink }, children: activeSubscription
|
|
108
124
|
? "You've used all your credits for this period."
|
|
109
|
-
: title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize:
|
|
125
|
+
: title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize: 13, color: theme.inkMuted }, children: activeSubscription
|
|
110
126
|
? `Your ${activeSubscription.planName} credits renew on ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()}.`
|
|
111
127
|
: description }), activeSubscription && (_jsxs("div", { style: {
|
|
112
128
|
marginTop: 16,
|
|
113
129
|
padding: "12px 16px",
|
|
114
|
-
borderRadius:
|
|
130
|
+
borderRadius: theme.radiusCard,
|
|
115
131
|
background: "#f5f5f7",
|
|
116
|
-
border: `1px solid ${
|
|
117
|
-
}, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 600, color:
|
|
132
|
+
border: `1px solid ${theme.hairline}`,
|
|
133
|
+
}, children: [_jsx("div", { style: { fontSize: 13, fontWeight: 600, color: theme.ink }, children: cancelDone ? "Subscription canceled" : `${activeSubscription.planName} · ${activeSubscription.planPeriod}` }), !cancelDone && (_jsx("button", { onClick: handleCancelSubscription, disabled: cancelPending, style: {
|
|
118
134
|
marginTop: 8,
|
|
119
135
|
appearance: "none",
|
|
120
136
|
border: "none",
|
|
@@ -122,61 +138,41 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
122
138
|
cursor: cancelPending ? "default" : "pointer",
|
|
123
139
|
padding: 0,
|
|
124
140
|
fontSize: 13,
|
|
125
|
-
color:
|
|
126
|
-
fontFamily:
|
|
141
|
+
color: theme.inkMuted,
|
|
142
|
+
fontFamily: theme.font,
|
|
127
143
|
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: {
|
|
144
|
+
}, children: cancelPending ? "Canceling…" : "Cancel subscription" }))] })), !activeSubscription && subscriptionPlans && subscriptionPlans.length > 0 && (_jsxs("div", { style: { marginTop: 24 }, children: [_jsx("div", { style: sectionLabelStyle(8), children: "Subscribe" }), _jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: subscriptionPlans.map((plan) => (_jsxs("button", { onClick: () => onSelectPlan?.(plan), style: {
|
|
136
145
|
...buttonStyle,
|
|
137
146
|
flex: "none",
|
|
138
147
|
display: "flex",
|
|
139
148
|
justifyContent: "space-between",
|
|
140
149
|
alignItems: "center",
|
|
141
|
-
}, children: [_jsx("span", { children: plan.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [
|
|
142
|
-
|
|
143
|
-
|
|
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: {
|
|
150
|
+
}, children: [_jsx("span", { children: plan.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [formatMoney(plan.price, currency), "/", plan.period === "monthly" ? "mo" : "yr", " \u00B7 ", formatCredits(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: sectionLabelStyle(-2), children: "Or buy credits" })), activeSubscription && packages && packages.length > 0 && (_jsx("div", { style: sectionLabelStyle(-2), children: "Add extra credits" })), packages && packages.length > 0
|
|
151
|
+
? 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: [formatMoney(pkg.price, currency), " \u00B7 ", formatCredits(pkg.credits), " cr"] })] }, pkg.id)))
|
|
152
|
+
: (!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: 14, color: theme.inkMuted }, children: currencySymbol }), _jsx("input", { value: customValue, onChange: (e) => setCustomValue(e.target.value), inputMode: "decimal", placeholder: String(customMin), style: {
|
|
158
153
|
flex: 1,
|
|
159
154
|
minWidth: 0,
|
|
160
155
|
appearance: "none",
|
|
161
|
-
fontFamily:
|
|
162
|
-
fontSize:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
156
|
+
fontFamily: theme.font,
|
|
157
|
+
fontSize: 14,
|
|
158
|
+
lineHeight: "20px",
|
|
159
|
+
padding: "8px 12px",
|
|
160
|
+
borderRadius: theme.radiusPill,
|
|
161
|
+
border: `1px solid ${theme.hairline}`,
|
|
162
|
+
color: theme.ink,
|
|
163
|
+
background: theme.canvas,
|
|
168
164
|
outline: "none",
|
|
169
|
-
} }), _jsx("button", { onClick: submitCustom, style: { ...buttonStyle, flex: "none" }, children:
|
|
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: {
|
|
170
166
|
width: "100%",
|
|
171
167
|
marginTop: 12,
|
|
172
168
|
appearance: "none",
|
|
173
169
|
border: "none",
|
|
174
170
|
cursor: "pointer",
|
|
175
171
|
background: "transparent",
|
|
176
|
-
color:
|
|
177
|
-
fontFamily:
|
|
178
|
-
fontSize:
|
|
179
|
-
padding: "
|
|
180
|
-
borderRadius:
|
|
181
|
-
}, children: "Not now" })) : null] }) }));
|
|
172
|
+
color: theme.inkMuted,
|
|
173
|
+
fontFamily: theme.font,
|
|
174
|
+
fontSize: 12,
|
|
175
|
+
padding: "8px 16px",
|
|
176
|
+
borderRadius: theme.radiusPill,
|
|
177
|
+
}, children: "Not now" })) : null, !hideBranding && (_jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 10 }, children: _jsx("span", { style: { fontSize: 10, color: theme.inkMuted }, children: "Powered by Crediball." }) }))] }) }));
|
|
182
178
|
}
|
package/dist/creditEvent.d.ts
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* This decouples the signal source from the UI, so the paywall works regardless
|
|
9
|
-
* of how the HTTP request was made (fetch, XHR, EventSource, AI SDK internals…).
|
|
2
|
+
* Re-exported from @crediball/core so the event bus is shared between
|
|
3
|
+
* @crediball/react, @crediball/elements, and any core-based data client —
|
|
4
|
+
* a signal fired from one surface (e.g. the web-components layer) is seen by
|
|
5
|
+
* every other. Kept as a distinct module path for backwards compatibility
|
|
6
|
+
* with existing imports inside this package.
|
|
10
7
|
*/
|
|
11
|
-
|
|
12
|
-
* Fire the global insufficient-credits signal. Safe to call from anywhere in
|
|
13
|
-
* the app — server components excluded (DOM only). Every mounted paywall
|
|
14
|
-
* component will react to it automatically.
|
|
15
|
-
*/
|
|
16
|
-
export declare function notifyInsufficientCredits(): void;
|
|
17
|
-
/**
|
|
18
|
-
* Subscribe to the global insufficient-credits signal.
|
|
19
|
-
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
20
|
-
*/
|
|
21
|
-
export declare function subscribeInsufficientCredits(listener: () => void): () => void;
|
|
8
|
+
export { notifyInsufficientCredits, subscribeInsufficientCredits } from "@crediball/core";
|
package/dist/creditEvent.js
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* This decouples the signal source from the UI, so the paywall works regardless
|
|
9
|
-
* of how the HTTP request was made (fetch, XHR, EventSource, AI SDK internals…).
|
|
2
|
+
* Re-exported from @crediball/core so the event bus is shared between
|
|
3
|
+
* @crediball/react, @crediball/elements, and any core-based data client —
|
|
4
|
+
* a signal fired from one surface (e.g. the web-components layer) is seen by
|
|
5
|
+
* every other. Kept as a distinct module path for backwards compatibility
|
|
6
|
+
* with existing imports inside this package.
|
|
10
7
|
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Fire the global insufficient-credits signal. Safe to call from anywhere in
|
|
14
|
-
* the app — server components excluded (DOM only). Every mounted paywall
|
|
15
|
-
* component will react to it automatically.
|
|
16
|
-
*/
|
|
17
|
-
export function notifyInsufficientCredits() {
|
|
18
|
-
if (typeof window === "undefined")
|
|
19
|
-
return;
|
|
20
|
-
window.dispatchEvent(new CustomEvent(EVENT));
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Subscribe to the global insufficient-credits signal.
|
|
24
|
-
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
25
|
-
*/
|
|
26
|
-
export function subscribeInsufficientCredits(listener) {
|
|
27
|
-
if (typeof window === "undefined")
|
|
28
|
-
return () => { };
|
|
29
|
-
window.addEventListener(EVENT, listener);
|
|
30
|
-
return () => window.removeEventListener(EVENT, listener);
|
|
31
|
-
}
|
|
8
|
+
export { notifyInsufficientCredits, subscribeInsufficientCredits } from "@crediball/core";
|
package/dist/fetchWatcher.d.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Patches window.fetch at module-import time — before any React component
|
|
3
|
+
* mounts and before any AI SDK can capture the fetch reference. This is the
|
|
4
|
+
* key difference from installing the patch in a useEffect: by the time a
|
|
5
|
+
* useEffect runs, the Vercel AI SDK (and others) may have already resolved
|
|
6
|
+
* their fetch reference and bypass the patch entirely.
|
|
6
7
|
*
|
|
7
|
-
* Handles
|
|
8
|
-
* 1. HTTP 402 — regular JSON
|
|
9
|
-
* 2. HTTP 200
|
|
10
|
-
*
|
|
8
|
+
* Handles three credit-error surfaces:
|
|
9
|
+
* 1. HTTP 402 with { code: "insufficient_credits" } — regular JSON routes.
|
|
10
|
+
* 2. HTTP 200 streaming body containing "insufficient_credits" — SSE, ndjson,
|
|
11
|
+
* or Vercel AI SDK data-stream (text/plain) routes.
|
|
12
|
+
* 3. Manual call to notifyInsufficientCredits() from any error handler.
|
|
11
13
|
*/
|
|
12
14
|
type Listener = () => void;
|
|
13
15
|
/**
|
|
14
16
|
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
15
|
-
* Returns an unsubscribe function
|
|
17
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
18
|
+
*
|
|
19
|
+
* The fetch patch is installed at module load time regardless of whether
|
|
20
|
+
* anyone calls this function. This exists for components that need a direct
|
|
21
|
+
* callback rather than the global DOM event.
|
|
16
22
|
*/
|
|
17
23
|
export declare function subscribeFetchWatcher(listener: Listener): () => void;
|
|
18
24
|
export {};
|
package/dist/fetchWatcher.js
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Patches window.fetch at module-import time — before any React component
|
|
3
|
+
* mounts and before any AI SDK can capture the fetch reference. This is the
|
|
4
|
+
* key difference from installing the patch in a useEffect: by the time a
|
|
5
|
+
* useEffect runs, the Vercel AI SDK (and others) may have already resolved
|
|
6
|
+
* their fetch reference and bypass the patch entirely.
|
|
6
7
|
*
|
|
7
|
-
* Handles
|
|
8
|
-
* 1. HTTP 402 — regular JSON
|
|
9
|
-
* 2. HTTP 200
|
|
10
|
-
*
|
|
8
|
+
* Handles three credit-error surfaces:
|
|
9
|
+
* 1. HTTP 402 with { code: "insufficient_credits" } — regular JSON routes.
|
|
10
|
+
* 2. HTTP 200 streaming body containing "insufficient_credits" — SSE, ndjson,
|
|
11
|
+
* or Vercel AI SDK data-stream (text/plain) routes.
|
|
12
|
+
* 3. Manual call to notifyInsufficientCredits() from any error handler.
|
|
11
13
|
*/
|
|
12
14
|
import { notifyInsufficientCredits } from "./creditEvent";
|
|
13
|
-
let _original = null;
|
|
14
15
|
const _listeners = new Set();
|
|
15
16
|
function fire() {
|
|
16
|
-
// Dispatch the global event so ALL mounted paywall instances react,
|
|
17
|
-
// plus any manual subscribeInsufficientCredits() listeners.
|
|
18
17
|
notifyInsufficientCredits();
|
|
19
|
-
// Also call per-instance listeners registered via subscribeFetchWatcher
|
|
20
|
-
// (kept for backwards compatibility with direct watcher subscribers).
|
|
21
18
|
_listeners.forEach((fn) => fn());
|
|
22
19
|
}
|
|
20
|
+
function isStreamingContentType(ct) {
|
|
21
|
+
return (ct.includes("text/event-stream") ||
|
|
22
|
+
ct.includes("ndjson") ||
|
|
23
|
+
// Vercel AI SDK uses text/plain for its data-stream protocol
|
|
24
|
+
ct.includes("text/plain"));
|
|
25
|
+
}
|
|
23
26
|
/**
|
|
24
27
|
* Tee the streaming response body and scan each chunk for the
|
|
25
28
|
* `insufficient_credits` signal. Returns a new Response whose body is the
|
|
@@ -54,10 +57,11 @@ function watchStream(res) {
|
|
|
54
57
|
headers: res.headers,
|
|
55
58
|
});
|
|
56
59
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// ── Install patch immediately at module load time ─────────────────────────
|
|
61
|
+
// Running here (not in a useEffect) ensures the patch is in place before any
|
|
62
|
+
// SDK or framework code captures the window.fetch reference.
|
|
63
|
+
if (typeof window !== "undefined") {
|
|
64
|
+
const _original = window.fetch;
|
|
61
65
|
window.fetch = async (...args) => {
|
|
62
66
|
const res = await _original(...args);
|
|
63
67
|
if (res.status === 402) {
|
|
@@ -72,43 +76,31 @@ function install() {
|
|
|
72
76
|
}
|
|
73
77
|
})
|
|
74
78
|
.catch(() => {
|
|
75
|
-
// Non-JSON 402 — fire anyway
|
|
76
|
-
// almost certainly a credit shortage. Set watchFetch=false and call
|
|
77
|
-
// trigger() manually if your app uses 402 for other purposes.
|
|
79
|
+
// Non-JSON 402 — fire anyway.
|
|
78
80
|
fire();
|
|
79
81
|
});
|
|
80
82
|
return res;
|
|
81
83
|
}
|
|
82
|
-
// Watch streaming responses for embedded credit errors (HTTP 200 with
|
|
83
|
-
// text/event-stream or ndjson — the common AI streaming pattern).
|
|
84
84
|
if (res.body) {
|
|
85
85
|
const ct = res.headers.get("content-type") ?? "";
|
|
86
|
-
if (
|
|
86
|
+
if (isStreamingContentType(ct)) {
|
|
87
87
|
return watchStream(res);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
return res;
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
-
function uninstall() {
|
|
94
|
-
if (typeof window === "undefined" || _original === null)
|
|
95
|
-
return;
|
|
96
|
-
window.fetch = _original;
|
|
97
|
-
_original = null;
|
|
98
|
-
}
|
|
99
93
|
/**
|
|
100
94
|
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
101
|
-
* Returns an unsubscribe function
|
|
95
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
96
|
+
*
|
|
97
|
+
* The fetch patch is installed at module load time regardless of whether
|
|
98
|
+
* anyone calls this function. This exists for components that need a direct
|
|
99
|
+
* callback rather than the global DOM event.
|
|
102
100
|
*/
|
|
103
101
|
export function subscribeFetchWatcher(listener) {
|
|
104
102
|
if (typeof window === "undefined")
|
|
105
103
|
return () => { };
|
|
106
|
-
if (_listeners.size === 0)
|
|
107
|
-
install();
|
|
108
104
|
_listeners.add(listener);
|
|
109
|
-
return () =>
|
|
110
|
-
_listeners.delete(listener);
|
|
111
|
-
if (_listeners.size === 0)
|
|
112
|
-
uninstall();
|
|
113
|
-
};
|
|
105
|
+
return () => _listeners.delete(listener);
|
|
114
106
|
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Formats a raw credit balance as a locale-formatted whole number (e.g. 840 → "840"). */
|
|
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;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Formats a raw credit balance as a locale-formatted whole number (e.g. 840 → "840"). */
|
|
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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { CreditsBadge, type CreditsBadgeProps } from "./CreditsBadge";
|
|
2
2
|
export { PaywallModal, type PaywallModalProps, type PackageOption, type SubscriptionPlanOption, type ActiveSubscriptionInfo, } from "./PaywallModal";
|
|
3
3
|
export { CreditIndicator, type CreditIndicatorProps } from "./CreditIndicator";
|
|
4
|
-
export { CrediballProvider, useCrediball, type CrediballProviderProps, } from "./CrediballProvider";
|
|
4
|
+
export { CrediballProvider, useCrediball, useCrediballOptional, type CrediballProviderProps, } from "./CrediballProvider";
|
|
5
5
|
export { usePaywall, isInsufficientCreditsError, type UsePaywallProps, type UsePaywallResult, } from "./usePaywall";
|
|
6
|
-
export { tokens, type CrediballThemeOverrides } from "./theme";
|
|
6
|
+
export { tokens, theme, cssVars, accentStyle, type CrediballThemeOverrides } from "./theme";
|
|
7
7
|
export { notifyInsufficientCredits } from "./creditEvent";
|
|
8
|
+
export { CrediballBalance, type CrediballBalanceProps } from "./variables/CrediballBalance";
|
|
9
|
+
export { CrediballUsage, type CrediballUsageProps } from "./variables/CrediballUsage";
|
|
10
|
+
export { CrediballTopUpButton, type CrediballTopUpButtonProps } from "./variables/CrediballTopUpButton";
|
|
11
|
+
export { CrediballLowCreditWarning, type CrediballLowCreditWarningProps, } from "./variables/CrediballLowCreditWarning";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
export { CreditsBadge } from "./CreditsBadge";
|
|
4
4
|
export { PaywallModal, } from "./PaywallModal";
|
|
5
5
|
export { CreditIndicator } from "./CreditIndicator";
|
|
6
|
-
export { CrediballProvider, useCrediball, } from "./CrediballProvider";
|
|
6
|
+
export { CrediballProvider, useCrediball, useCrediballOptional, } from "./CrediballProvider";
|
|
7
7
|
export { usePaywall, isInsufficientCreditsError, } from "./usePaywall";
|
|
8
|
-
export { tokens } from "./theme";
|
|
8
|
+
export { tokens, theme, cssVars, accentStyle } from "./theme";
|
|
9
9
|
export { notifyInsufficientCredits } from "./creditEvent";
|
|
10
|
+
// Tier 2 — "variables" you place anywhere inside <CrediballProvider>.
|
|
11
|
+
export { CrediballBalance } from "./variables/CrediballBalance";
|
|
12
|
+
export { CrediballUsage } from "./variables/CrediballUsage";
|
|
13
|
+
export { CrediballTopUpButton } from "./variables/CrediballTopUpButton";
|
|
14
|
+
export { CrediballLowCreditWarning, } from "./variables/CrediballLowCreditWarning";
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
1
2
|
/**
|
|
2
3
|
* Self-contained design tokens (no Tailwind / external CSS) so these components
|
|
3
4
|
* look right when dropped into any host app. Mirrors the Crediball design
|
|
@@ -19,3 +20,41 @@ export type CrediballThemeOverrides = {
|
|
|
19
20
|
/** Override the accent color (defaults to Action Blue #0066cc). */
|
|
20
21
|
accentColor?: string;
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* `--crediball-*` CSS custom property names. A host app can set these once
|
|
25
|
+
* (on `:root` or any ancestor) to theme every embedded component — including
|
|
26
|
+
* ones rendered later, or across `@crediball/elements` web components — without
|
|
27
|
+
* passing props. Components read these via `theme` below, which wraps each
|
|
28
|
+
* token in `var(--crediball-x, <tokens fallback>)`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const cssVars: {
|
|
31
|
+
readonly accent: "--crediball-accent";
|
|
32
|
+
readonly ink: "--crediball-ink";
|
|
33
|
+
readonly inkMuted: "--crediball-ink-muted";
|
|
34
|
+
readonly canvas: "--crediball-canvas";
|
|
35
|
+
readonly hairline: "--crediball-hairline";
|
|
36
|
+
readonly onAccent: "--crediball-on-accent";
|
|
37
|
+
readonly overlay: "--crediball-overlay";
|
|
38
|
+
readonly radiusPill: "--crediball-radius";
|
|
39
|
+
readonly radiusCard: "--crediball-radius-card";
|
|
40
|
+
readonly font: "--crediball-font";
|
|
41
|
+
};
|
|
42
|
+
/** CSS-var-wrapped values for use in component styles — prefer these over raw `tokens`. */
|
|
43
|
+
export declare const theme: {
|
|
44
|
+
accent: string;
|
|
45
|
+
ink: string;
|
|
46
|
+
inkMuted: string;
|
|
47
|
+
canvas: string;
|
|
48
|
+
hairline: string;
|
|
49
|
+
onAccent: string;
|
|
50
|
+
overlay: string;
|
|
51
|
+
radiusPill: string;
|
|
52
|
+
radiusCard: string;
|
|
53
|
+
font: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Sets `--crediball-accent` on a component's root element when `accentColor`
|
|
57
|
+
* is passed, so the prop keeps working exactly as before while everything
|
|
58
|
+
* underneath consistently reads `theme.accent`. Spread onto the root `style`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function accentStyle(accentColor?: string): CSSProperties;
|
package/dist/theme.js
CHANGED
|
@@ -15,3 +15,46 @@ export const tokens = {
|
|
|
15
15
|
radiusPill: 9999,
|
|
16
16
|
radiusCard: 18,
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* `--crediball-*` CSS custom property names. A host app can set these once
|
|
20
|
+
* (on `:root` or any ancestor) to theme every embedded component — including
|
|
21
|
+
* ones rendered later, or across `@crediball/elements` web components — without
|
|
22
|
+
* passing props. Components read these via `theme` below, which wraps each
|
|
23
|
+
* token in `var(--crediball-x, <tokens fallback>)`.
|
|
24
|
+
*/
|
|
25
|
+
export const cssVars = {
|
|
26
|
+
accent: "--crediball-accent",
|
|
27
|
+
ink: "--crediball-ink",
|
|
28
|
+
inkMuted: "--crediball-ink-muted",
|
|
29
|
+
canvas: "--crediball-canvas",
|
|
30
|
+
hairline: "--crediball-hairline",
|
|
31
|
+
onAccent: "--crediball-on-accent",
|
|
32
|
+
overlay: "--crediball-overlay",
|
|
33
|
+
radiusPill: "--crediball-radius",
|
|
34
|
+
radiusCard: "--crediball-radius-card",
|
|
35
|
+
font: "--crediball-font",
|
|
36
|
+
};
|
|
37
|
+
function v(name, fallback) {
|
|
38
|
+
return `var(${name}, ${fallback})`;
|
|
39
|
+
}
|
|
40
|
+
/** CSS-var-wrapped values for use in component styles — prefer these over raw `tokens`. */
|
|
41
|
+
export const theme = {
|
|
42
|
+
accent: v(cssVars.accent, tokens.primary),
|
|
43
|
+
ink: v(cssVars.ink, tokens.ink),
|
|
44
|
+
inkMuted: v(cssVars.inkMuted, tokens.inkMuted),
|
|
45
|
+
canvas: v(cssVars.canvas, tokens.canvas),
|
|
46
|
+
hairline: v(cssVars.hairline, tokens.hairline),
|
|
47
|
+
onAccent: v(cssVars.onAccent, tokens.onPrimary),
|
|
48
|
+
overlay: v(cssVars.overlay, tokens.overlay),
|
|
49
|
+
radiusPill: v(cssVars.radiusPill, `${tokens.radiusPill}px`),
|
|
50
|
+
radiusCard: v(cssVars.radiusCard, `${tokens.radiusCard}px`),
|
|
51
|
+
font: v(cssVars.font, tokens.fontStack),
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Sets `--crediball-accent` on a component's root element when `accentColor`
|
|
55
|
+
* is passed, so the prop keeps working exactly as before while everything
|
|
56
|
+
* underneath consistently reads `theme.accent`. Spread onto the root `style`.
|
|
57
|
+
*/
|
|
58
|
+
export function accentStyle(accentColor) {
|
|
59
|
+
return accentColor ? { [cssVars.accent]: accentColor } : {};
|
|
60
|
+
}
|