@crediball/react 0.1.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/README.md +78 -0
- package/dist/CrediballProvider.d.ts +54 -0
- package/dist/CrediballProvider.js +65 -0
- package/dist/CreditIndicator.d.ts +14 -0
- package/dist/CreditIndicator.js +24 -0
- package/dist/CreditsBadge.d.ts +27 -0
- package/dist/CreditsBadge.js +40 -0
- package/dist/PaywallModal.d.ts +32 -0
- package/dist/PaywallModal.js +123 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/theme.d.ts +21 -0
- package/dist/theme.js +17 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @crediball/react
|
|
2
|
+
|
|
3
|
+
Drop-in, **unbranded** React components for showing
|
|
4
|
+
[Crediball](https://github.com/filipporezzadore/Crediball) credits inside your AI
|
|
5
|
+
app — a credit balance, a top-bar chip, and an automatic paywall. Self-contained
|
|
6
|
+
(no CSS framework), and your end users never see Crediball branding or log into
|
|
7
|
+
Crediball.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @crediball/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> `react >= 18` is a peer dependency. Pair this with
|
|
14
|
+
> [`@crediball/sdk`](https://www.npmjs.com/package/@crediball/sdk), which does the
|
|
15
|
+
> server-side metering.
|
|
16
|
+
|
|
17
|
+
## Automatic paywall (recommended)
|
|
18
|
+
|
|
19
|
+
Wrap your app once. When any request returns `402` with
|
|
20
|
+
`{ code: "insufficient_credits" }` (your server forwarding Crediball's error), the
|
|
21
|
+
paywall opens automatically — no per-call wiring.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { CrediballProvider } from "@crediball/react";
|
|
25
|
+
|
|
26
|
+
<CrediballProvider
|
|
27
|
+
amounts={[5, 10, 20]} // top-up options (match your dashboard packages)
|
|
28
|
+
currencySymbol="€"
|
|
29
|
+
onTopup={(amount) => checkout(amount)} // wire to your checkout / top-up route
|
|
30
|
+
>
|
|
31
|
+
<App />
|
|
32
|
+
</CrediballProvider>;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Open or close it yourself from anywhere inside the provider:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useCrediball } from "@crediball/react";
|
|
39
|
+
|
|
40
|
+
const { showPaywall, hidePaywall, open } = useCrediball();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Set `watchFetch={false}` on the provider to disable auto-detection and drive the
|
|
44
|
+
paywall only via `showPaywall()`.
|
|
45
|
+
|
|
46
|
+
## Components
|
|
47
|
+
|
|
48
|
+
| Export | Use |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `CrediballProvider` | App-wide paywall + auto-detect of `402 insufficient_credits`. |
|
|
51
|
+
| `useCrediball()` | `{ showPaywall, hidePaywall, open }` from inside the provider. |
|
|
52
|
+
| `CreditsBadge` | A balance block for a settings page (big number + optional ≈ money value). |
|
|
53
|
+
| `CreditIndicator` | A quiet balance chip for the top bar; clickable to open top-up. |
|
|
54
|
+
| `PaywallModal` | The standalone paywall dialog (used by the provider; usable directly). |
|
|
55
|
+
| `tokens` | The default theme tokens (colors, fonts) if you want to match the look. |
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { CreditsBadge, CreditIndicator, PaywallModal } from "@crediball/react";
|
|
59
|
+
|
|
60
|
+
// Settings page
|
|
61
|
+
<CreditsBadge credits={840} rate={100} currency="EUR" onAddCredits={openTopUp} />
|
|
62
|
+
|
|
63
|
+
// Top bar
|
|
64
|
+
<CreditIndicator balanceLabel="6.20" onClick={openTopUp} />
|
|
65
|
+
|
|
66
|
+
// Manual paywall
|
|
67
|
+
<PaywallModal open={broke} amounts={[5, 10]} onAdd={topUp} onClose={close} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
All components accept theme overrides (e.g. `accentColor`) and inline `style`, and
|
|
71
|
+
render without any CSS framework.
|
|
72
|
+
|
|
73
|
+
## Principles
|
|
74
|
+
- **Unbranded.** Crediball never appears to your end users.
|
|
75
|
+
- **Client-side UI only.** Metering and your `cb_live_` key stay on the server —
|
|
76
|
+
see [`@crediball/sdk`](https://www.npmjs.com/package/@crediball/sdk).
|
|
77
|
+
|
|
78
|
+
MIT © Crediball
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface CrediballProviderProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/**
|
|
5
|
+
* Top-up amounts to offer in the paywall (host decides the meaning, e.g. euros).
|
|
6
|
+
* Match these to the packages you configured in the Crediball dashboard.
|
|
7
|
+
*/
|
|
8
|
+
amounts?: number[];
|
|
9
|
+
/**
|
|
10
|
+
* Called when the user picks an amount to top up. Wire this to your checkout or
|
|
11
|
+
* server-side top-up route. If omitted, picking an amount just closes the modal.
|
|
12
|
+
*/
|
|
13
|
+
onTopup?: (amount: number) => void | Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Auto-detect insufficient-credit responses (HTTP 402 with
|
|
16
|
+
* `{ code: "insufficient_credits" }`) from any fetch() call and open the paywall
|
|
17
|
+
* automatically — no per-call wiring. Defaults to true. Set false to only open
|
|
18
|
+
* the paywall manually via useCrediball().showPaywall().
|
|
19
|
+
*/
|
|
20
|
+
watchFetch?: boolean;
|
|
21
|
+
/** Allow a free-form amount entry in the paywall. */
|
|
22
|
+
allowCustom?: boolean;
|
|
23
|
+
customMin?: number;
|
|
24
|
+
customMax?: number;
|
|
25
|
+
/** Currency symbol shown in the paywall (default "€"). Match your app's billing currency. */
|
|
26
|
+
currencySymbol?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Override the accent color (defaults to Action Blue). */
|
|
30
|
+
accentColor?: string;
|
|
31
|
+
}
|
|
32
|
+
interface CrediballContextValue {
|
|
33
|
+
/** Open the paywall modal. */
|
|
34
|
+
showPaywall: () => void;
|
|
35
|
+
/** Close the paywall modal. */
|
|
36
|
+
hidePaywall: () => void;
|
|
37
|
+
/** Whether the paywall is currently open. */
|
|
38
|
+
open: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pattern B, automated. Wrap your app once:
|
|
42
|
+
*
|
|
43
|
+
* <CrediballProvider amounts={[5, 15, 40]} onTopup={(eur) => checkout(eur)}>
|
|
44
|
+
* <App />
|
|
45
|
+
* </CrediballProvider>
|
|
46
|
+
*
|
|
47
|
+
* Whenever any request returns 402 `insufficient_credits` (your server forwarding
|
|
48
|
+
* Crediball's error), the paywall appears automatically — no extra code per call.
|
|
49
|
+
* You can also open it yourself with useCrediball().showPaywall().
|
|
50
|
+
*/
|
|
51
|
+
export declare function CrediballProvider({ children, amounts, onTopup, watchFetch, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
|
|
52
|
+
/** Access the paywall controls from anywhere inside <CrediballProvider>. */
|
|
53
|
+
export declare function useCrediball(): CrediballContextValue;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
4
|
+
import { PaywallModal } from "./PaywallModal";
|
|
5
|
+
const CrediballContext = createContext(null);
|
|
6
|
+
/**
|
|
7
|
+
* Pattern B, automated. Wrap your app once:
|
|
8
|
+
*
|
|
9
|
+
* <CrediballProvider amounts={[5, 15, 40]} onTopup={(eur) => checkout(eur)}>
|
|
10
|
+
* <App />
|
|
11
|
+
* </CrediballProvider>
|
|
12
|
+
*
|
|
13
|
+
* Whenever any request returns 402 `insufficient_credits` (your server forwarding
|
|
14
|
+
* Crediball's error), the paywall appears automatically — no extra code per call.
|
|
15
|
+
* You can also open it yourself with useCrediball().showPaywall().
|
|
16
|
+
*/
|
|
17
|
+
export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, watchFetch = true, allowCustom = false, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
const showPaywall = useCallback(() => setOpen(true), []);
|
|
20
|
+
const hidePaywall = useCallback(() => setOpen(false), []);
|
|
21
|
+
// Auto-open on any 402 insufficient_credits response, app-wide. We patch
|
|
22
|
+
// window.fetch and inspect a *clone* so the original response is untouched.
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!watchFetch || typeof window === "undefined")
|
|
25
|
+
return;
|
|
26
|
+
const original = window.fetch;
|
|
27
|
+
window.fetch = async (...args) => {
|
|
28
|
+
const res = await original(...args);
|
|
29
|
+
if (res.status === 402) {
|
|
30
|
+
res
|
|
31
|
+
.clone()
|
|
32
|
+
.json()
|
|
33
|
+
.then((body) => {
|
|
34
|
+
if (body && body.code === "insufficient_credits")
|
|
35
|
+
setOpen(true);
|
|
36
|
+
})
|
|
37
|
+
.catch(() => {
|
|
38
|
+
/* non-JSON 402: ignore */
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return res;
|
|
42
|
+
};
|
|
43
|
+
return () => {
|
|
44
|
+
window.fetch = original;
|
|
45
|
+
};
|
|
46
|
+
}, [watchFetch]);
|
|
47
|
+
const ctx = useMemo(() => ({ showPaywall, hidePaywall, open }), [showPaywall, hidePaywall, open]);
|
|
48
|
+
async function handleAdd(amount) {
|
|
49
|
+
try {
|
|
50
|
+
await onTopup?.(amount);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
setOpen(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return (_jsxs(CrediballContext.Provider, { value: ctx, children: [children, _jsx(PaywallModal, { open: open, amounts: amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: allowCustom, customMin: customMin, customMax: customMax, currencySymbol: currencySymbol, title: title, description: description, accentColor: accentColor })] }));
|
|
57
|
+
}
|
|
58
|
+
/** Access the paywall controls from anywhere inside <CrediballProvider>. */
|
|
59
|
+
export function useCrediball() {
|
|
60
|
+
const ctx = useContext(CrediballContext);
|
|
61
|
+
if (!ctx) {
|
|
62
|
+
throw new Error("useCrediball must be used inside <CrediballProvider>.");
|
|
63
|
+
}
|
|
64
|
+
return ctx;
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
export interface CreditIndicatorProps {
|
|
3
|
+
/** Pre-formatted balance (e.g. "6.20"). */
|
|
4
|
+
balanceLabel: string;
|
|
5
|
+
/** Text before the balance (default "Credits:"). */
|
|
6
|
+
label?: string;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pattern C — Subtle indicator. A quiet balance chip for a host app's top bar;
|
|
12
|
+
* clickable to open a top-up flow. Minimal and non-intrusive.
|
|
13
|
+
*/
|
|
14
|
+
export declare function CreditIndicator({ balanceLabel, label, onClick, style, }: CreditIndicatorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { tokens } from "./theme";
|
|
4
|
+
/**
|
|
5
|
+
* Pattern C — Subtle indicator. A quiet balance chip for a host app's top bar;
|
|
6
|
+
* clickable to open a top-up flow. Minimal and non-intrusive.
|
|
7
|
+
*/
|
|
8
|
+
export function CreditIndicator({ balanceLabel, label = "Credits:", onClick, style, }) {
|
|
9
|
+
return (_jsxs("button", { onClick: onClick, style: {
|
|
10
|
+
display: "inline-flex",
|
|
11
|
+
alignItems: "center",
|
|
12
|
+
gap: 4,
|
|
13
|
+
appearance: "none",
|
|
14
|
+
cursor: onClick ? "pointer" : "default",
|
|
15
|
+
background: tokens.canvas,
|
|
16
|
+
border: `1px solid ${tokens.hairline}`,
|
|
17
|
+
borderRadius: tokens.radiusPill,
|
|
18
|
+
color: tokens.ink,
|
|
19
|
+
fontFamily: tokens.fontStack,
|
|
20
|
+
fontSize: 14,
|
|
21
|
+
padding: "4px 12px",
|
|
22
|
+
...style,
|
|
23
|
+
}, children: [_jsx("span", { style: { color: tokens.inkMuted }, children: label }), _jsx("span", { style: { fontWeight: 600 }, children: balanceLabel })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { type CrediballThemeOverrides } from "./theme";
|
|
3
|
+
export interface CreditsBadgeProps extends CrediballThemeOverrides {
|
|
4
|
+
/** The user's raw credit balance (the big number). */
|
|
5
|
+
credits: number;
|
|
6
|
+
/**
|
|
7
|
+
* Credits per one unit of currency. When set, a derived money value is shown
|
|
8
|
+
* underneath (e.g. credits=840, rate=100 → "≈ €8.40"). Omit to hide it.
|
|
9
|
+
*/
|
|
10
|
+
rate?: number;
|
|
11
|
+
/** ISO 4217 currency for the derived value (default "EUR"). */
|
|
12
|
+
currency?: string;
|
|
13
|
+
/** Label shown above the balance. */
|
|
14
|
+
label?: string;
|
|
15
|
+
/** Called when the user clicks "Add credits". */
|
|
16
|
+
onAddCredits?: () => void;
|
|
17
|
+
/** Override the button text. */
|
|
18
|
+
addLabel?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pattern A — Embedded (recommended). Drop into a profile/settings section to
|
|
23
|
+
* show the user's balance with an "Add credits" action. Shows the credit balance
|
|
24
|
+
* as the primary figure and an approximate money value beneath it. No Crediball
|
|
25
|
+
* branding.
|
|
26
|
+
*/
|
|
27
|
+
export declare function CreditsBadge({ credits, rate, currency, label, onAddCredits, addLabel, accentColor, style, }: CreditsBadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { tokens } from "./theme";
|
|
4
|
+
function formatCredits(credits) {
|
|
5
|
+
return new Intl.NumberFormat(undefined).format(Math.round(credits));
|
|
6
|
+
}
|
|
7
|
+
function formatMoney(amount, currency) {
|
|
8
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(amount);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Pattern A — Embedded (recommended). Drop into a profile/settings section to
|
|
12
|
+
* show the user's balance with an "Add credits" action. Shows the credit balance
|
|
13
|
+
* as the primary figure and an approximate money value beneath it. No Crediball
|
|
14
|
+
* branding.
|
|
15
|
+
*/
|
|
16
|
+
export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit balance", onAddCredits, addLabel = "Add credits", accentColor = tokens.primary, style, }) {
|
|
17
|
+
const showMoney = typeof rate === "number" && rate > 0;
|
|
18
|
+
return (_jsxs("div", { style: {
|
|
19
|
+
display: "flex",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "space-between",
|
|
22
|
+
gap: 16,
|
|
23
|
+
padding: 24,
|
|
24
|
+
border: `1px solid ${tokens.hairline}`,
|
|
25
|
+
borderRadius: tokens.radiusCard,
|
|
26
|
+
background: tokens.canvas,
|
|
27
|
+
fontFamily: tokens.fontStack,
|
|
28
|
+
...style,
|
|
29
|
+
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, color: tokens.inkMuted }, children: label }), _jsxs("div", { style: { fontSize: 28, fontWeight: 600, color: tokens.ink, letterSpacing: "-0.01em" }, children: [formatCredits(credits), _jsx("span", { style: { fontSize: 16, fontWeight: 400, color: tokens.inkMuted, marginLeft: 6 }, children: "credits" })] }), showMoney ? (_jsxs("div", { style: { fontSize: 13, color: tokens.inkMuted, marginTop: 2 }, children: ["\u2248 ", formatMoney(credits / rate, currency)] })) : null] }), _jsx("button", { onClick: onAddCredits, style: {
|
|
30
|
+
appearance: "none",
|
|
31
|
+
border: "none",
|
|
32
|
+
cursor: "pointer",
|
|
33
|
+
background: accentColor,
|
|
34
|
+
color: tokens.onPrimary,
|
|
35
|
+
fontFamily: tokens.fontStack,
|
|
36
|
+
fontSize: 14,
|
|
37
|
+
padding: "8px 18px",
|
|
38
|
+
borderRadius: tokens.radiusPill,
|
|
39
|
+
}, children: addLabel })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import { type CrediballThemeOverrides } from "./theme";
|
|
3
|
+
export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
4
|
+
open: boolean;
|
|
5
|
+
/** Top-up amounts to offer (host decides the meaning, e.g. euros). */
|
|
6
|
+
amounts?: number[];
|
|
7
|
+
/** Prefix shown on each button before the amount (default "Add €"). */
|
|
8
|
+
amountPrefix?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
onAdd?: (amount: number) => void;
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
/** When true, show a free-form amount field in addition to the preset buttons. */
|
|
14
|
+
allowCustom?: boolean;
|
|
15
|
+
/** Minimum custom amount (default 1). */
|
|
16
|
+
customMin?: number;
|
|
17
|
+
/** Optional maximum custom amount. */
|
|
18
|
+
customMax?: number;
|
|
19
|
+
/** Currency symbol shown beside the custom field (default "€"). */
|
|
20
|
+
currencySymbol?: string;
|
|
21
|
+
/**
|
|
22
|
+
* When provided, a "Your balance" row is shown at the top of the modal.
|
|
23
|
+
* Useful for the paywall context so the user can see why the modal appeared.
|
|
24
|
+
*/
|
|
25
|
+
balance?: number;
|
|
26
|
+
/** Credits per one currency unit — used to derive the ≈ money value beside the balance. */
|
|
27
|
+
balanceRate?: number;
|
|
28
|
+
/** ISO 4217 currency for the derived balance money value (default "EUR"). */
|
|
29
|
+
currency?: string;
|
|
30
|
+
style?: CSSProperties;
|
|
31
|
+
}
|
|
32
|
+
export declare function PaywallModal({ open, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, customMin, customMax, currencySymbol, balance, balanceRate, currency, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { tokens } from "./theme";
|
|
5
|
+
/**
|
|
6
|
+
* Pattern B — Paywall moment. Render when a user has insufficient credits to
|
|
7
|
+
* continue. The host controls when it appears; no Crediball branding.
|
|
8
|
+
*/
|
|
9
|
+
function fmtCredits(n) {
|
|
10
|
+
return new Intl.NumberFormat().format(Math.round(n));
|
|
11
|
+
}
|
|
12
|
+
function fmtMoney(n, currency) {
|
|
13
|
+
try {
|
|
14
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(n);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return `${n.toFixed(2)} ${currency}`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function PaywallModal({ open, 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
|
+
const [narrow, setNarrow] = useState(false);
|
|
23
|
+
const [customValue, setCustomValue] = useState("");
|
|
24
|
+
const [customError, setCustomError] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (typeof window === "undefined" || !window.matchMedia)
|
|
27
|
+
return;
|
|
28
|
+
const mq = window.matchMedia("(max-width: 480px)");
|
|
29
|
+
const update = () => setNarrow(mq.matches);
|
|
30
|
+
update();
|
|
31
|
+
mq.addEventListener("change", update);
|
|
32
|
+
return () => mq.removeEventListener("change", update);
|
|
33
|
+
}, []);
|
|
34
|
+
if (!open)
|
|
35
|
+
return null;
|
|
36
|
+
function submitCustom() {
|
|
37
|
+
const amt = Number(customValue);
|
|
38
|
+
if (!Number.isFinite(amt) || amt <= 0) {
|
|
39
|
+
setCustomError("Enter a valid amount.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (amt < customMin) {
|
|
43
|
+
setCustomError(`Minimum is ${currencySymbol}${customMin}.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (customMax != null && amt > customMax) {
|
|
47
|
+
setCustomError(`Maximum is ${currencySymbol}${customMax}.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setCustomError(null);
|
|
51
|
+
onAdd?.(amt);
|
|
52
|
+
}
|
|
53
|
+
const buttonStyle = {
|
|
54
|
+
flex: 1,
|
|
55
|
+
appearance: "none",
|
|
56
|
+
border: "none",
|
|
57
|
+
cursor: "pointer",
|
|
58
|
+
background: accentColor,
|
|
59
|
+
color: tokens.onPrimary,
|
|
60
|
+
fontFamily: tokens.fontStack,
|
|
61
|
+
fontSize: 17,
|
|
62
|
+
padding: "11px 22px",
|
|
63
|
+
borderRadius: tokens.radiusPill,
|
|
64
|
+
};
|
|
65
|
+
return (_jsx("div", { role: "dialog", "aria-modal": "true", style: {
|
|
66
|
+
position: "fixed",
|
|
67
|
+
inset: 0,
|
|
68
|
+
zIndex: 1000,
|
|
69
|
+
display: "flex",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
padding: 24,
|
|
73
|
+
background: tokens.overlay,
|
|
74
|
+
fontFamily: tokens.fontStack,
|
|
75
|
+
}, onClick: onClose, children: _jsxs("div", { onClick: (e) => e.stopPropagation(), style: {
|
|
76
|
+
width: "100%",
|
|
77
|
+
maxWidth: 420,
|
|
78
|
+
padding: 32,
|
|
79
|
+
borderRadius: tokens.radiusCard,
|
|
80
|
+
background: tokens.canvas,
|
|
81
|
+
...style,
|
|
82
|
+
}, children: [balance != null && (_jsxs("div", { style: {
|
|
83
|
+
marginBottom: 24,
|
|
84
|
+
paddingBottom: 20,
|
|
85
|
+
borderBottom: `1px solid ${tokens.hairline}`,
|
|
86
|
+
}, children: [_jsx("div", { style: {
|
|
87
|
+
fontSize: 11,
|
|
88
|
+
fontWeight: 600,
|
|
89
|
+
letterSpacing: "0.06em",
|
|
90
|
+
textTransform: "uppercase",
|
|
91
|
+
color: tokens.inkMuted,
|
|
92
|
+
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: narrow ? "column" : "row",
|
|
96
|
+
gap: 12,
|
|
97
|
+
marginTop: 24,
|
|
98
|
+
}, 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: {
|
|
99
|
+
flex: 1,
|
|
100
|
+
minWidth: 0,
|
|
101
|
+
appearance: "none",
|
|
102
|
+
fontFamily: tokens.fontStack,
|
|
103
|
+
fontSize: 17,
|
|
104
|
+
padding: "11px 14px",
|
|
105
|
+
borderRadius: tokens.radiusPill,
|
|
106
|
+
border: `1px solid ${tokens.hairline}`,
|
|
107
|
+
color: tokens.ink,
|
|
108
|
+
background: tokens.canvas,
|
|
109
|
+
outline: "none",
|
|
110
|
+
} }), _jsx("button", { onClick: submitCustom, style: { ...buttonStyle, flex: "none" }, children: "Add" })] }), customError ? (_jsx("p", { style: { margin: "8px 0 0", fontSize: 13, color: accentColor }, children: customError })) : null] })) : null, onClose ? (_jsx("button", { onClick: onClose, style: {
|
|
111
|
+
width: "100%",
|
|
112
|
+
marginTop: 12,
|
|
113
|
+
appearance: "none",
|
|
114
|
+
border: "none",
|
|
115
|
+
cursor: "pointer",
|
|
116
|
+
background: "transparent",
|
|
117
|
+
color: tokens.inkMuted,
|
|
118
|
+
fontFamily: tokens.fontStack,
|
|
119
|
+
fontSize: 14,
|
|
120
|
+
padding: "11px 22px",
|
|
121
|
+
borderRadius: tokens.radiusPill,
|
|
122
|
+
}, children: "Not now" })) : null] }) }));
|
|
123
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CreditsBadge, type CreditsBadgeProps } from "./CreditsBadge";
|
|
2
|
+
export { PaywallModal, type PaywallModalProps } from "./PaywallModal";
|
|
3
|
+
export { CreditIndicator, type CreditIndicatorProps } from "./CreditIndicator";
|
|
4
|
+
export { CrediballProvider, useCrediball, type CrediballProviderProps, } from "./CrediballProvider";
|
|
5
|
+
export { tokens, type CrediballThemeOverrides } from "./theme";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @crediball/react — drop-in, self-contained components for showing credits
|
|
2
|
+
// inside a host AI app. No Crediball branding, no end-user login, no Tailwind.
|
|
3
|
+
export { CreditsBadge } from "./CreditsBadge";
|
|
4
|
+
export { PaywallModal } from "./PaywallModal";
|
|
5
|
+
export { CreditIndicator } from "./CreditIndicator";
|
|
6
|
+
export { CrediballProvider, useCrediball, } from "./CrediballProvider";
|
|
7
|
+
export { tokens } from "./theme";
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained design tokens (no Tailwind / external CSS) so these components
|
|
3
|
+
* look right when dropped into any host app. Mirrors the Crediball design
|
|
4
|
+
* language: single Action-Blue accent, pill CTAs, parchment/ink neutrals.
|
|
5
|
+
*/
|
|
6
|
+
export declare const tokens: {
|
|
7
|
+
primary: string;
|
|
8
|
+
ink: string;
|
|
9
|
+
inkMuted: string;
|
|
10
|
+
onPrimary: string;
|
|
11
|
+
canvas: string;
|
|
12
|
+
hairline: string;
|
|
13
|
+
overlay: string;
|
|
14
|
+
fontStack: string;
|
|
15
|
+
radiusPill: number;
|
|
16
|
+
radiusCard: number;
|
|
17
|
+
};
|
|
18
|
+
export type CrediballThemeOverrides = {
|
|
19
|
+
/** Override the accent color (defaults to Action Blue #0066cc). */
|
|
20
|
+
accentColor?: string;
|
|
21
|
+
};
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained design tokens (no Tailwind / external CSS) so these components
|
|
3
|
+
* look right when dropped into any host app. Mirrors the Crediball design
|
|
4
|
+
* language: single Action-Blue accent, pill CTAs, parchment/ink neutrals.
|
|
5
|
+
*/
|
|
6
|
+
export const tokens = {
|
|
7
|
+
primary: "#0066cc",
|
|
8
|
+
ink: "#1d1d1f",
|
|
9
|
+
inkMuted: "#7a7a7a",
|
|
10
|
+
onPrimary: "#ffffff",
|
|
11
|
+
canvas: "#ffffff",
|
|
12
|
+
hairline: "#e0e0e0",
|
|
13
|
+
overlay: "rgba(0, 0, 0, 0.4)",
|
|
14
|
+
fontStack: 'system-ui, -apple-system, "Segoe UI", Roboto, Inter, Helvetica, Arial, sans-serif',
|
|
15
|
+
radiusPill: 9999,
|
|
16
|
+
radiusCard: 18,
|
|
17
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crediball/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drop-in React components for showing Crediball credits inside your AI app.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
|
|
7
|
+
"homepage": "https://crediball.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/filipporezzadore/Crediball.git",
|
|
11
|
+
"directory": "packages/react"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/filipporezzadore/Crediball/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react",
|
|
18
|
+
"components",
|
|
19
|
+
"paywall",
|
|
20
|
+
"credits",
|
|
21
|
+
"billing",
|
|
22
|
+
"ui",
|
|
23
|
+
"ai"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "^18.3.11",
|
|
55
|
+
"typescript": "^5.6.3"
|
|
56
|
+
}
|
|
57
|
+
}
|