@crediball/react 0.3.0 → 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.
@@ -2,10 +2,8 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
4
4
  import { PaywallModal } from "./PaywallModal";
5
+ import { subscribeFetchWatcher } from "./fetchWatcher";
5
6
  const CrediballContext = createContext(null);
6
- // Symbol used to mark a patched fetch so double-mount in React Strict Mode (dev)
7
- // or a second <CrediballProvider> doesn't stack intercepts.
8
- const CREDIBALL_FETCH_PATCHED = Symbol.for("crediball.fetch.patched");
9
7
  /**
10
8
  * Pattern B, automated. Wrap your app once:
11
9
  *
@@ -22,46 +20,15 @@ const CREDIBALL_FETCH_PATCHED = Symbol.for("crediball.fetch.patched");
22
20
  */
23
21
  export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, watchFetch = true, allowCustom = false, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
24
22
  const [open, setOpen] = useState(false);
25
- // Stable callback refs so the fetch interceptor closure doesn't go stale.
26
23
  const showPaywall = useCallback(() => setOpen(true), []);
27
24
  const hidePaywall = useCallback(() => setOpen(false), []);
25
+ // Keep a ref so the fetch-watcher closure always calls the latest version.
28
26
  const showPaywallRef = useRef(showPaywall);
29
27
  showPaywallRef.current = showPaywall;
30
28
  useEffect(() => {
31
- if (!watchFetch || typeof window === "undefined")
29
+ if (!watchFetch)
32
30
  return;
33
- // If another provider (or a double-invoke from React Strict Mode) already
34
- // patched this window.fetch in the same tick, skip — the cleanup will restore.
35
- const win = window;
36
- if (win[CREDIBALL_FETCH_PATCHED])
37
- return;
38
- const original = window.fetch;
39
- win[CREDIBALL_FETCH_PATCHED] = true;
40
- window.fetch = async (...args) => {
41
- const res = await original(...args);
42
- if (res.status === 402) {
43
- // Clone so the original response body is still readable by the caller.
44
- res
45
- .clone()
46
- .json()
47
- .then((body) => {
48
- if (body && body.code === "insufficient_credits") {
49
- showPaywallRef.current();
50
- }
51
- })
52
- .catch(() => {
53
- // Non-JSON 402 (e.g. Stripe webhook): open paywall anyway since any
54
- // 402 from the app's own API routes is very likely a credit shortage.
55
- // If the app has other 402 uses, set watchFetch=false and call
56
- // showPaywall() manually.
57
- });
58
- }
59
- return res;
60
- };
61
- return () => {
62
- window.fetch = original;
63
- delete win[CREDIBALL_FETCH_PATCHED];
64
- };
31
+ return subscribeFetchWatcher(() => showPaywallRef.current());
65
32
  }, [watchFetch]);
66
33
  const ctx = useMemo(() => ({ showPaywall, hidePaywall, open }), [showPaywall, hidePaywall, open]);
67
34
  async function handleAdd(amount) {
@@ -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;
@@ -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) {
@@ -65,7 +79,7 @@ export function PaywallModal({ open, packages, onSelectPackage, amounts = [5, 10
65
79
  return (_jsx("div", { role: "dialog", "aria-modal": "true", style: {
66
80
  position: "fixed",
67
81
  inset: 0,
68
- zIndex: 1000,
82
+ zIndex: 2147483647,
69
83
  display: "flex",
70
84
  alignItems: "center",
71
85
  justifyContent: "center",
@@ -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: title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize: 14, color: tokens.inkMuted }, children: description }), _jsx("div", { style: {
94
- display: "flex",
95
- flexDirection: "column",
96
- gap: 10,
97
- marginTop: 24,
98
- }, children: packages && packages.length > 0
99
- ? 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)))
100
- : (_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: {
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",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Module-level singleton that patches window.fetch once and fans out to all
3
+ * registered listeners (from CrediballProvider and/or usePaywall). Only one
4
+ * fetch patch exists at a time regardless of how many components subscribe.
5
+ * Reference-counted: the patch is removed when the last listener unsubscribes.
6
+ */
7
+ type Listener = () => void;
8
+ /**
9
+ * Subscribe to insufficient-credits 402 events detected in window.fetch.
10
+ * Returns an unsubscribe function — call it in your useEffect cleanup.
11
+ */
12
+ export declare function subscribeFetchWatcher(listener: Listener): () => void;
13
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Module-level singleton that patches window.fetch once and fans out to all
3
+ * registered listeners (from CrediballProvider and/or usePaywall). Only one
4
+ * fetch patch exists at a time regardless of how many components subscribe.
5
+ * Reference-counted: the patch is removed when the last listener unsubscribes.
6
+ */
7
+ let _original = null;
8
+ const _listeners = new Set();
9
+ function install() {
10
+ if (typeof window === "undefined" || _original !== null)
11
+ return;
12
+ _original = window.fetch;
13
+ window.fetch = async (...args) => {
14
+ const res = await _original(...args);
15
+ if (res.status === 402) {
16
+ res
17
+ .clone()
18
+ .json()
19
+ .then((body) => {
20
+ if (body != null &&
21
+ typeof body === "object" &&
22
+ body.code === "insufficient_credits") {
23
+ _listeners.forEach((fn) => fn());
24
+ }
25
+ })
26
+ .catch(() => {
27
+ // Non-JSON 402. Fire listeners anyway — a 402 from the app's own API
28
+ // route is almost certainly a credit shortage. Set watchFetch=false
29
+ // and call trigger() manually if your app uses 402 for other purposes.
30
+ _listeners.forEach((fn) => fn());
31
+ });
32
+ }
33
+ return res;
34
+ };
35
+ }
36
+ function uninstall() {
37
+ if (typeof window === "undefined" || _original === null)
38
+ return;
39
+ window.fetch = _original;
40
+ _original = null;
41
+ }
42
+ /**
43
+ * Subscribe to insufficient-credits 402 events detected in window.fetch.
44
+ * Returns an unsubscribe function — call it in your useEffect cleanup.
45
+ */
46
+ export function subscribeFetchWatcher(listener) {
47
+ if (typeof window === "undefined")
48
+ return () => { };
49
+ if (_listeners.size === 0)
50
+ install();
51
+ _listeners.add(listener);
52
+ return () => {
53
+ _listeners.delete(listener);
54
+ if (_listeners.size === 0)
55
+ uninstall();
56
+ };
57
+ }
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";
@@ -5,6 +5,19 @@ export type UsePaywallProps = Pick<PaywallModalProps, "amounts" | "amountPrefix"
5
5
  onTopup?: (amount: number) => void | Promise<void>;
6
6
  /** Called when the user picks a package from `packages`. */
7
7
  onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
8
+ /**
9
+ * Auto-detect HTTP 402 `insufficient_credits` responses from any fetch() call
10
+ * and open the paywall automatically. Defaults to false.
11
+ *
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.
16
+ *
17
+ * Note: SSE/streaming routes surface credit errors as stream events (HTTP 200),
18
+ * not HTTP 402 — call trigger() manually from your stream error handler.
19
+ */
20
+ watchFetch?: boolean;
8
21
  };
9
22
  export interface UsePaywallResult {
10
23
  /**
@@ -57,4 +70,4 @@ export interface UsePaywallResult {
57
70
  *
58
71
  * return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
59
72
  */
60
- export declare function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, }?: UsePaywallProps): UsePaywallResult;
73
+ export declare function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch, }?: UsePaywallProps): UsePaywallResult;
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { PaywallModal } from "./PaywallModal";
5
+ import { subscribeFetchWatcher } from "./fetchWatcher";
5
6
  /**
6
7
  * Manages a Crediball paywall modal with a simple, misuse-proof API.
7
8
  *
@@ -27,10 +28,18 @@ import { PaywallModal } from "./PaywallModal";
27
28
  *
28
29
  * return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
29
30
  */
30
- export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, } = {}) {
31
+ export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch = false, } = {}) {
31
32
  const [isOpen, setIsOpen] = useState(false);
32
33
  const trigger = useCallback(() => setIsOpen(true), []);
33
34
  const dismiss = useCallback(() => setIsOpen(false), []);
35
+ // Keep a ref so the fetch-watcher closure always calls the latest trigger.
36
+ const triggerRef = useRef(trigger);
37
+ triggerRef.current = trigger;
38
+ useEffect(() => {
39
+ if (!watchFetch)
40
+ return;
41
+ return subscribeFetchWatcher(() => triggerRef.current());
42
+ }, [watchFetch]);
34
43
  const handleTopup = useCallback(async (amount) => {
35
44
  setIsOpen(false);
36
45
  await onTopup?.(amount);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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>",