@crediball/react 0.3.7 → 0.5.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 +85 -9
- package/dist/CrediballProvider.js +157 -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/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
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import { type PackageOption, type SubscriptionPlanOption } from "./PaywallModal";
|
|
3
|
+
import { type UsagePeriod } from "@crediball/core";
|
|
2
4
|
export interface CrediballProviderProps {
|
|
3
5
|
children: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Browser-safe key (cb_pub_...) from the Crediball dashboard. When set
|
|
8
|
+
* together with `userId`, the provider fetches and live-updates balance,
|
|
9
|
+
* usage, event costs, and packages — powering useCrediball()'s data fields,
|
|
10
|
+
* the Tier-2 variable components, and PaywallModal's package list. Omit both
|
|
11
|
+
* to use the provider in its original, data-less mode (host supplies
|
|
12
|
+
* everything via props/callbacks).
|
|
13
|
+
*/
|
|
14
|
+
publishableKey?: string;
|
|
15
|
+
/** The end user's id, exactly as passed to track() server-side. Required alongside publishableKey. */
|
|
16
|
+
userId?: string;
|
|
17
|
+
/** Base URL of the Crediball API. Defaults to the hosted instance. */
|
|
18
|
+
apiUrl?: string;
|
|
19
|
+
/** Poll interval while the tab is visible. Default 30s. */
|
|
20
|
+
pollIntervalMs?: number;
|
|
21
|
+
/** Override the default "low credits" threshold (defaults to the highest-cost active event). */
|
|
22
|
+
lowCreditThreshold?: number;
|
|
4
23
|
/**
|
|
5
24
|
* Top-up amounts to offer in the paywall (host decides the meaning, e.g. euros).
|
|
6
25
|
* Match these to the packages you configured in the Crediball dashboard.
|
|
26
|
+
* Ignored once real packages are available (with publishableKey/userId set).
|
|
7
27
|
*/
|
|
8
28
|
amounts?: number[];
|
|
9
29
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
30
|
+
* Optional override for the custom-amount top-up. If omitted, the paywall
|
|
31
|
+
* starts a Crediball-hosted Stripe Checkout itself (using publishableKey +
|
|
32
|
+
* userId) and redirects — no backend wiring needed, just connect payouts in the
|
|
33
|
+
* dashboard. Provide this only to run your own top-up/checkout flow instead.
|
|
12
34
|
*/
|
|
13
35
|
onTopup?: (amount: number) => void | Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional override for one-time packages. If omitted, the paywall starts a
|
|
38
|
+
* Crediball-hosted Stripe Checkout itself and redirects (connect payouts in the
|
|
39
|
+
* dashboard). Provide this only to run your own flow (e.g. `meter.topup({ userId,
|
|
40
|
+
* packageId })` server-side).
|
|
41
|
+
*/
|
|
42
|
+
onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Called when the user picks a subscription plan. Unlike one-time top-ups,
|
|
45
|
+
* subscriptions have NO built-in checkout (they need recurring billing your
|
|
46
|
+
* backend sets up), so the paywall only shows subscription plans when this is
|
|
47
|
+
* provided — wire it to `meter.subscribe({ userId, planId })` server-side.
|
|
48
|
+
*/
|
|
49
|
+
onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
|
|
50
|
+
/** Called when the user cancels their subscription. Call `meter.cancelSubscription({ userId, subscriptionId })` server-side. */
|
|
51
|
+
onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
|
|
14
52
|
/**
|
|
15
53
|
* Auto-detect insufficient-credit responses from any fetch() call and open the
|
|
16
54
|
* paywall automatically — no per-call wiring. Defaults to true. Set false to only
|
|
@@ -20,9 +58,15 @@ export interface CrediballProviderProps {
|
|
|
20
58
|
* ndjson (streaming/SSE routes where the error arrives as a stream chunk).
|
|
21
59
|
*/
|
|
22
60
|
watchFetch?: boolean;
|
|
23
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Show a free-form amount field in the paywall. Defaults to your dashboard's
|
|
63
|
+
* custom-topup setting (with its min/max) when packages are loaded — set this
|
|
64
|
+
* explicitly only to override (e.g. `false` to force-hide it).
|
|
65
|
+
*/
|
|
24
66
|
allowCustom?: boolean;
|
|
67
|
+
/** Override the custom-amount minimum (defaults to the dashboard's configured minimum). */
|
|
25
68
|
customMin?: number;
|
|
69
|
+
/** Override the custom-amount maximum (defaults to the dashboard's configured maximum). */
|
|
26
70
|
customMax?: number;
|
|
27
71
|
/** Currency symbol shown in the paywall (default "€"). Match your app's billing currency. */
|
|
28
72
|
currencySymbol?: string;
|
|
@@ -42,28 +86,60 @@ interface CrediballContextValue {
|
|
|
42
86
|
hidePaywall: () => void;
|
|
43
87
|
/** Whether the paywall is currently open. */
|
|
44
88
|
open: boolean;
|
|
89
|
+
/** Alias of showPaywall — reads better from Tier-2 variable components. */
|
|
90
|
+
openPaywall: () => void;
|
|
91
|
+
/** The user's current credit balance. null until loaded, or always null without publishableKey/userId. */
|
|
92
|
+
balance: number | null;
|
|
93
|
+
/** Credits consumed in the user's current period (subscription cycle, or calendar month). */
|
|
94
|
+
usage: UsagePeriod | null;
|
|
95
|
+
/** Alias of balance — the credits left to spend. */
|
|
96
|
+
remaining: number | null;
|
|
97
|
+
/** True once balance drops below the low-credit threshold. */
|
|
98
|
+
isLow: boolean;
|
|
99
|
+
/** True while the initial fetch is in flight. */
|
|
100
|
+
loading: boolean;
|
|
101
|
+
/** Set when the last fetch failed. */
|
|
102
|
+
error: Error | null;
|
|
103
|
+
/** Credit cost of a tracked event, from the live cost catalog. undefined if unknown. */
|
|
104
|
+
costOf: (event: string) => number | undefined;
|
|
105
|
+
/** Trigger a top-up (delegates to the `onTopup` prop) and refresh balance/usage on completion. */
|
|
106
|
+
topUp: (amount: number) => Promise<void>;
|
|
107
|
+
/** Refetch balance, usage, costs, and packages immediately. No-op without publishableKey/userId. */
|
|
108
|
+
refresh: () => Promise<void>;
|
|
45
109
|
}
|
|
46
110
|
/**
|
|
47
111
|
* Pattern B, automated. Wrap your app once:
|
|
48
112
|
*
|
|
49
|
-
* <CrediballProvider
|
|
113
|
+
* <CrediballProvider
|
|
114
|
+
* publishableKey="cb_pub_..."
|
|
115
|
+
* userId={user.id}
|
|
116
|
+
* onTopup={(eur) => checkout(eur)}
|
|
117
|
+
* >
|
|
50
118
|
* <App />
|
|
51
119
|
* </CrediballProvider>
|
|
52
120
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
121
|
+
* With `publishableKey` + `userId` set, every useCrediball() consumer and every
|
|
122
|
+
* Tier-2 variable component (<CrediballBalance/>, <CrediballUsage/>, ...) gets
|
|
123
|
+
* live balance/usage/cost data automatically. Whenever any request returns 402
|
|
124
|
+
* `insufficient_credits`, the paywall appears automatically — no extra code per
|
|
125
|
+
* call. You can also open it yourself with useCrediball().showPaywall().
|
|
56
126
|
*
|
|
57
127
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
58
128
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
59
129
|
*/
|
|
60
|
-
export declare function CrediballProvider({ children, amounts, onTopup, watchFetch, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
|
|
130
|
+
export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
|
|
61
131
|
/**
|
|
62
|
-
* Access the paywall
|
|
132
|
+
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
63
133
|
*
|
|
64
134
|
* Important: showPaywall() is always safe to call — never add a "paywallAlreadyShown"
|
|
65
135
|
* guard around it. Dismissing the modal ("Not now") does not prevent future calls
|
|
66
136
|
* from reopening it. Call showPaywall() unconditionally on every InsufficientCreditsError.
|
|
67
137
|
*/
|
|
68
138
|
export declare function useCrediball(): CrediballContextValue;
|
|
139
|
+
/**
|
|
140
|
+
* Same as useCrediball(), but returns null instead of throwing outside a
|
|
141
|
+
* <CrediballProvider>. For components (like CreditsBadge) that work either
|
|
142
|
+
* standalone via props or auto-wired via a provider.
|
|
143
|
+
*/
|
|
144
|
+
export declare function useCrediballOptional(): CrediballContextValue | null;
|
|
69
145
|
export {};
|
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import { PaywallModal } from "./PaywallModal";
|
|
5
5
|
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
6
6
|
import { subscribeInsufficientCredits } from "./creditEvent";
|
|
7
|
+
import { getCrediballClient, notifyBalanceChanged } from "@crediball/core";
|
|
7
8
|
const CrediballContext = createContext(null);
|
|
9
|
+
const EMPTY_SNAPSHOT = {
|
|
10
|
+
balance: null,
|
|
11
|
+
usage: null,
|
|
12
|
+
remaining: null,
|
|
13
|
+
costs: {},
|
|
14
|
+
packages: null,
|
|
15
|
+
isLow: false,
|
|
16
|
+
loading: false,
|
|
17
|
+
error: null,
|
|
18
|
+
};
|
|
19
|
+
const noopSubscribe = () => () => { };
|
|
20
|
+
const getEmptySnapshot = () => EMPTY_SNAPSHOT;
|
|
8
21
|
/**
|
|
9
22
|
* Pattern B, automated. Wrap your app once:
|
|
10
23
|
*
|
|
11
|
-
* <CrediballProvider
|
|
24
|
+
* <CrediballProvider
|
|
25
|
+
* publishableKey="cb_pub_..."
|
|
26
|
+
* userId={user.id}
|
|
27
|
+
* onTopup={(eur) => checkout(eur)}
|
|
28
|
+
* >
|
|
12
29
|
* <App />
|
|
13
30
|
* </CrediballProvider>
|
|
14
31
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
32
|
+
* With `publishableKey` + `userId` set, every useCrediball() consumer and every
|
|
33
|
+
* Tier-2 variable component (<CrediballBalance/>, <CrediballUsage/>, ...) gets
|
|
34
|
+
* live balance/usage/cost data automatically. Whenever any request returns 402
|
|
35
|
+
* `insufficient_credits`, the paywall appears automatically — no extra code per
|
|
36
|
+
* call. You can also open it yourself with useCrediball().showPaywall().
|
|
18
37
|
*
|
|
19
38
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
20
39
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
21
40
|
*/
|
|
22
|
-
export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, watchFetch = true, allowCustom
|
|
41
|
+
export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, watchFetch = true, allowCustom, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
|
|
23
42
|
const [open, setOpen] = useState(false);
|
|
24
43
|
const showPaywall = useCallback(() => setOpen(true), []);
|
|
25
44
|
const hidePaywall = useCallback(() => setOpen(false), []);
|
|
@@ -35,19 +54,139 @@ export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, wa
|
|
|
35
54
|
return;
|
|
36
55
|
return subscribeFetchWatcher(() => showPaywallRef.current());
|
|
37
56
|
}, [watchFetch]);
|
|
38
|
-
|
|
57
|
+
// Shared data client — one poller/stream per (publishableKey, userId, apiUrl),
|
|
58
|
+
// reused by every component reading useCrediball() or a Tier-2 variable.
|
|
59
|
+
const client = useMemo(() => {
|
|
60
|
+
if (!publishableKey || !userId)
|
|
61
|
+
return null;
|
|
62
|
+
return getCrediballClient({ publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold });
|
|
63
|
+
}, [publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!client)
|
|
66
|
+
return;
|
|
67
|
+
client.start();
|
|
68
|
+
return () => client.stop();
|
|
69
|
+
}, [client]);
|
|
70
|
+
const snapshot = useSyncExternalStore(client ? client.subscribe : noopSubscribe, client ? client.getSnapshot : getEmptySnapshot, client ? client.getSnapshot : getEmptySnapshot);
|
|
71
|
+
const costOf = useCallback((event) => client?.costOf(event), [client]);
|
|
72
|
+
const refresh = useCallback(async () => {
|
|
73
|
+
await client?.refresh();
|
|
74
|
+
}, [client]);
|
|
75
|
+
const topUp = useCallback(async (amount) => {
|
|
76
|
+
await onTopup?.(amount);
|
|
77
|
+
notifyBalanceChanged();
|
|
78
|
+
await client?.refresh();
|
|
79
|
+
}, [onTopup, client]);
|
|
80
|
+
const ctx = useMemo(() => ({
|
|
81
|
+
showPaywall,
|
|
82
|
+
hidePaywall,
|
|
83
|
+
open,
|
|
84
|
+
openPaywall: showPaywall,
|
|
85
|
+
balance: snapshot.balance,
|
|
86
|
+
usage: snapshot.usage,
|
|
87
|
+
remaining: snapshot.remaining,
|
|
88
|
+
isLow: snapshot.isLow,
|
|
89
|
+
loading: snapshot.loading,
|
|
90
|
+
error: snapshot.error,
|
|
91
|
+
costOf,
|
|
92
|
+
topUp,
|
|
93
|
+
refresh,
|
|
94
|
+
}), [showPaywall, hidePaywall, open, snapshot, costOf, topUp, refresh]);
|
|
95
|
+
// The page to return to after Stripe checkout — the one the user is on now.
|
|
96
|
+
function currentReturnPath() {
|
|
97
|
+
if (typeof window === "undefined")
|
|
98
|
+
return undefined;
|
|
99
|
+
return window.location.pathname + window.location.search;
|
|
100
|
+
}
|
|
101
|
+
// Drive a Crediball-hosted Stripe Checkout with just the publishable key — used
|
|
102
|
+
// when the host didn't wire its own onSelectPackage/onTopup handler, so the
|
|
103
|
+
// paywall's buttons work out of the box (the developer only has to connect
|
|
104
|
+
// payouts in the dashboard). Redirects the browser to Stripe on success.
|
|
105
|
+
async function startBuiltInCheckout(input) {
|
|
106
|
+
if (!client)
|
|
107
|
+
return;
|
|
108
|
+
const { url } = await client.createCheckout({ ...input, returnPath: currentReturnPath() });
|
|
109
|
+
if (typeof window !== "undefined")
|
|
110
|
+
window.location.assign(url);
|
|
111
|
+
}
|
|
39
112
|
async function handleAdd(amount) {
|
|
113
|
+
// A host-supplied top-up flow always wins.
|
|
114
|
+
if (onTopup) {
|
|
115
|
+
try {
|
|
116
|
+
await topUp(amount);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
setOpen(false);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Otherwise start Stripe ourselves (custom amount). Leave the modal open on
|
|
124
|
+
// failure so the error is visible instead of the modal silently closing.
|
|
125
|
+
if (client) {
|
|
126
|
+
try {
|
|
127
|
+
await startBuiltInCheckout({ eurAmount: amount });
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
console.error("[crediball] checkout failed:", err);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Data-less mode with no handler: nothing to charge against.
|
|
135
|
+
setOpen(false);
|
|
136
|
+
}
|
|
137
|
+
async function handleSelectPackage(pkg) {
|
|
138
|
+
if (onSelectPackage) {
|
|
139
|
+
try {
|
|
140
|
+
await onSelectPackage(pkg);
|
|
141
|
+
notifyBalanceChanged();
|
|
142
|
+
await client?.refresh();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
setOpen(false);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (client) {
|
|
150
|
+
try {
|
|
151
|
+
await startBuiltInCheckout({ packageId: pkg.id });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error("[crediball] checkout failed:", err);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setOpen(false);
|
|
159
|
+
}
|
|
160
|
+
async function handleSelectPlan(plan) {
|
|
40
161
|
try {
|
|
41
|
-
await
|
|
162
|
+
await onSelectPlan?.(plan);
|
|
163
|
+
notifyBalanceChanged();
|
|
164
|
+
await client?.refresh();
|
|
42
165
|
}
|
|
43
166
|
finally {
|
|
44
167
|
setOpen(false);
|
|
45
168
|
}
|
|
46
169
|
}
|
|
47
|
-
|
|
170
|
+
async function handleCancelSubscription(subscriptionId) {
|
|
171
|
+
await onCancelSubscription?.(subscriptionId);
|
|
172
|
+
await client?.refresh();
|
|
173
|
+
}
|
|
174
|
+
const pkgs = snapshot.packages;
|
|
175
|
+
// Auto-derive the custom-amount field from the dashboard's custom-topup config,
|
|
176
|
+
// so a developer who enabled it doesn't also have to pass allowCustom/min/max.
|
|
177
|
+
// Explicit props still win (allowCustom={false} force-hides it).
|
|
178
|
+
const customCfg = pkgs?.customTopup;
|
|
179
|
+
const effectiveAllowCustom = allowCustom ?? customCfg?.enabled ?? false;
|
|
180
|
+
const effectiveCustomMin = customMin ?? customCfg?.minEur;
|
|
181
|
+
const effectiveCustomMax = customMax ?? customCfg?.maxEur ?? undefined;
|
|
182
|
+
return (_jsxs(CrediballContext.Provider, { value: ctx, children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage,
|
|
183
|
+
// Subscriptions can't be fulfilled by the built-in checkout (that path is
|
|
184
|
+
// one-time top-ups only), so only OFFER them when the host wired an
|
|
185
|
+
// onSelectPlan to fulfill them — otherwise they'd be dead buttons.
|
|
186
|
+
subscriptionPlans: onSelectPlan ? pkgs?.subscriptionPlans : undefined, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: handleCancelSubscription, amounts: amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: effectiveAllowCustom, customMin: effectiveCustomMin, customMax: effectiveCustomMax, currencySymbol: currencySymbol, balance: snapshot.balance ?? undefined, balanceRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, currency: pkgs?.currency, usage: snapshot.usage?.usedCredits ?? undefined, title: title, description: description, accentColor: accentColor })] }));
|
|
48
187
|
}
|
|
49
188
|
/**
|
|
50
|
-
* Access the paywall
|
|
189
|
+
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
51
190
|
*
|
|
52
191
|
* Important: showPaywall() is always safe to call — never add a "paywallAlreadyShown"
|
|
53
192
|
* guard around it. Dismissing the modal ("Not now") does not prevent future calls
|
|
@@ -60,3 +199,11 @@ export function useCrediball() {
|
|
|
60
199
|
}
|
|
61
200
|
return ctx;
|
|
62
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Same as useCrediball(), but returns null instead of throwing outside a
|
|
204
|
+
* <CrediballProvider>. For components (like CreditsBadge) that work either
|
|
205
|
+
* standalone via props or auto-wired via a provider.
|
|
206
|
+
*/
|
|
207
|
+
export function useCrediballOptional() {
|
|
208
|
+
return useContext(CrediballContext);
|
|
209
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { type CrediballThemeOverrides } from "./theme";
|
|
3
|
+
export interface CreditIndicatorProps extends CrediballThemeOverrides {
|
|
4
|
+
/**
|
|
5
|
+
* Pre-formatted balance (e.g. "6.20"). Omit to read + format it automatically
|
|
6
|
+
* from an ancestor <CrediballProvider publishableKey userId>.
|
|
7
|
+
*/
|
|
8
|
+
balanceLabel?: string;
|
|
5
9
|
/** Text before the balance (default "Credits:"). */
|
|
6
10
|
label?: string;
|
|
11
|
+
/** Defaults to opening the provider's paywall when a <CrediballProvider> is present. */
|
|
7
12
|
onClick?: () => void;
|
|
8
13
|
style?: CSSProperties;
|
|
9
14
|
}
|
|
@@ -11,4 +16,4 @@ export interface CreditIndicatorProps {
|
|
|
11
16
|
* Pattern C — Subtle indicator. A quiet balance chip for a host app's top bar;
|
|
12
17
|
* clickable to open a top-up flow. Minimal and non-intrusive.
|
|
13
18
|
*/
|
|
14
|
-
export declare function CreditIndicator({ balanceLabel, label, onClick, style, }: CreditIndicatorProps): import("react").JSX.Element;
|
|
19
|
+
export declare function CreditIndicator({ balanceLabel, label, onClick, accentColor, style, }: CreditIndicatorProps): import("react").JSX.Element;
|
package/dist/CreditIndicator.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { theme, accentStyle } from "./theme";
|
|
4
|
+
import { formatCredits } from "./format";
|
|
5
|
+
import { useCrediballOptional } from "./CrediballProvider";
|
|
4
6
|
/**
|
|
5
7
|
* Pattern C — Subtle indicator. A quiet balance chip for a host app's top bar;
|
|
6
8
|
* clickable to open a top-up flow. Minimal and non-intrusive.
|
|
7
9
|
*/
|
|
8
|
-
export function CreditIndicator({ balanceLabel, label = "Credits:", onClick, style, }) {
|
|
9
|
-
|
|
10
|
+
export function CreditIndicator({ balanceLabel, label = "Credits:", onClick, accentColor, style, }) {
|
|
11
|
+
const ctx = useCrediballOptional();
|
|
12
|
+
const resolvedLabel = balanceLabel ?? (ctx?.balance != null ? formatCredits(ctx.balance) : "—");
|
|
13
|
+
const resolvedOnClick = onClick ?? ctx?.openPaywall;
|
|
14
|
+
return (_jsxs("button", { onClick: resolvedOnClick, style: {
|
|
10
15
|
display: "inline-flex",
|
|
11
16
|
alignItems: "center",
|
|
12
17
|
gap: 4,
|
|
13
18
|
appearance: "none",
|
|
14
|
-
cursor:
|
|
15
|
-
background:
|
|
16
|
-
border: `1px solid ${
|
|
17
|
-
borderRadius:
|
|
18
|
-
color:
|
|
19
|
-
fontFamily:
|
|
19
|
+
cursor: resolvedOnClick ? "pointer" : "default",
|
|
20
|
+
background: theme.canvas,
|
|
21
|
+
border: `1px solid ${theme.hairline}`,
|
|
22
|
+
borderRadius: theme.radiusPill,
|
|
23
|
+
color: theme.ink,
|
|
24
|
+
fontFamily: theme.font,
|
|
20
25
|
fontSize: 14,
|
|
21
26
|
padding: "4px 12px",
|
|
27
|
+
...accentStyle(accentColor),
|
|
22
28
|
...style,
|
|
23
|
-
}, children: [_jsx("span", { style: { color:
|
|
29
|
+
}, children: [_jsx("span", { style: { color: theme.inkMuted }, children: label }), _jsx("span", { style: { fontWeight: 600 }, children: resolvedLabel })] }));
|
|
24
30
|
}
|
package/dist/CreditsBadge.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { CSSProperties } from "react";
|
|
2
2
|
import { type CrediballThemeOverrides } from "./theme";
|
|
3
3
|
export interface CreditsBadgeProps extends CrediballThemeOverrides {
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The user's raw credit balance (the big number). Omit to read it
|
|
6
|
+
* automatically from an ancestor <CrediballProvider publishableKey userId>.
|
|
7
|
+
*/
|
|
8
|
+
credits?: number;
|
|
6
9
|
/**
|
|
7
10
|
* Credits per one unit of currency. When set, a derived money value is shown
|
|
8
11
|
* underneath (e.g. credits=840, rate=100 → "≈ €8.40"). Omit to hide it.
|
|
@@ -12,7 +15,7 @@ export interface CreditsBadgeProps extends CrediballThemeOverrides {
|
|
|
12
15
|
currency?: string;
|
|
13
16
|
/** Label shown above the balance. */
|
|
14
17
|
label?: string;
|
|
15
|
-
/** Called when the user clicks "Add credits". */
|
|
18
|
+
/** Called when the user clicks "Add credits". Defaults to opening the provider's paywall. */
|
|
16
19
|
onAddCredits?: () => void;
|
|
17
20
|
/** Override the button text. */
|
|
18
21
|
addLabel?: string;
|
package/dist/CreditsBadge.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
function formatMoney(amount, currency) {
|
|
8
|
-
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(amount);
|
|
9
|
-
}
|
|
3
|
+
import { theme, accentStyle } from "./theme";
|
|
4
|
+
import { formatCredits, formatMoney } from "./format";
|
|
5
|
+
import { useCrediballOptional } from "./CrediballProvider";
|
|
10
6
|
/**
|
|
11
7
|
* Pattern A — Embedded (recommended). Drop into a profile/settings section to
|
|
12
8
|
* show the user's balance with an "Add credits" action. Shows the credit balance
|
|
13
9
|
* as the primary figure and an approximate money value beneath it. No Crediball
|
|
14
10
|
* branding.
|
|
15
11
|
*/
|
|
16
|
-
export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit balance", onAddCredits, addLabel = "Add credits", accentColor
|
|
12
|
+
export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit balance", onAddCredits, addLabel = "Add credits", accentColor, style, }) {
|
|
13
|
+
const ctx = useCrediballOptional();
|
|
14
|
+
const resolvedCredits = credits ?? ctx?.balance ?? 0;
|
|
17
15
|
const showMoney = typeof rate === "number" && rate > 0;
|
|
18
16
|
return (_jsxs("div", { style: {
|
|
19
17
|
display: "flex",
|
|
@@ -21,20 +19,21 @@ export function CreditsBadge({ credits, rate, currency = "EUR", label = "Credit
|
|
|
21
19
|
justifyContent: "space-between",
|
|
22
20
|
gap: 16,
|
|
23
21
|
padding: 24,
|
|
24
|
-
border: `1px solid ${
|
|
25
|
-
borderRadius:
|
|
26
|
-
background:
|
|
27
|
-
fontFamily:
|
|
22
|
+
border: `1px solid ${theme.hairline}`,
|
|
23
|
+
borderRadius: theme.radiusCard,
|
|
24
|
+
background: theme.canvas,
|
|
25
|
+
fontFamily: theme.font,
|
|
26
|
+
...accentStyle(accentColor),
|
|
28
27
|
...style,
|
|
29
|
-
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, color:
|
|
28
|
+
}, children: [_jsxs("div", { children: [_jsx("div", { style: { fontSize: 14, color: theme.inkMuted }, children: label }), _jsxs("div", { style: { fontSize: 28, fontWeight: 600, color: theme.ink, letterSpacing: "-0.01em" }, children: [formatCredits(resolvedCredits), _jsx("span", { style: { fontSize: 16, fontWeight: 400, color: theme.inkMuted, marginLeft: 6 }, children: "credits" })] }), showMoney ? (_jsxs("div", { style: { fontSize: 13, color: theme.inkMuted, marginTop: 2 }, children: ["\u2248 ", formatMoney(resolvedCredits / rate, currency)] })) : null] }), _jsx("button", { onClick: onAddCredits ?? ctx?.openPaywall, style: {
|
|
30
29
|
appearance: "none",
|
|
31
30
|
border: "none",
|
|
32
31
|
cursor: "pointer",
|
|
33
|
-
background:
|
|
34
|
-
color:
|
|
35
|
-
fontFamily:
|
|
32
|
+
background: theme.accent,
|
|
33
|
+
color: theme.onAccent,
|
|
34
|
+
fontFamily: theme.font,
|
|
36
35
|
fontSize: 14,
|
|
37
|
-
padding: "8px
|
|
38
|
-
borderRadius:
|
|
36
|
+
padding: "8px 16px",
|
|
37
|
+
borderRadius: theme.radiusPill,
|
|
39
38
|
}, children: addLabel })] }));
|
|
40
39
|
}
|
package/dist/PaywallModal.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
72
72
|
onClose?: () => void;
|
|
73
73
|
/** When true, show a free-form amount field in addition to the preset buttons. */
|
|
74
74
|
allowCustom?: boolean;
|
|
75
|
+
/** Button label for the free-form amount field (default "Add"). */
|
|
76
|
+
addButtonText?: string;
|
|
75
77
|
/** Minimum custom amount (default 1). */
|
|
76
78
|
customMin?: number;
|
|
77
79
|
/** Optional maximum custom amount. */
|
|
@@ -79,14 +81,26 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
79
81
|
/** Currency symbol shown beside the custom field (default "€"). */
|
|
80
82
|
currencySymbol?: string;
|
|
81
83
|
/**
|
|
82
|
-
* When provided, a
|
|
83
|
-
*
|
|
84
|
+
* When provided, a compact "Balance" column is shown at the top of the
|
|
85
|
+
* modal (beside "Usage" if that's provided too) so the user can see why
|
|
86
|
+
* the modal appeared.
|
|
84
87
|
*/
|
|
85
88
|
balance?: number;
|
|
86
89
|
/** Credits per one currency unit — used to derive the ≈ money value beside the balance. */
|
|
87
90
|
balanceRate?: number;
|
|
88
91
|
/** ISO 4217 currency for the derived balance money value (default "EUR"). */
|
|
89
92
|
currency?: string;
|
|
93
|
+
/** Credits used this period — shown as a second column beside balance, when provided. */
|
|
94
|
+
usage?: number;
|
|
95
|
+
/** Label above the usage column (default "Usage"). */
|
|
96
|
+
usageLabel?: string;
|
|
97
|
+
/** Hide the "Powered by Crediball" footer line (default false). */
|
|
98
|
+
hideBranding?: boolean;
|
|
90
99
|
style?: CSSProperties;
|
|
91
100
|
}
|
|
92
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Pattern B — Paywall moment. Render when a user has insufficient credits to
|
|
103
|
+
* continue. The host controls when it appears; shows a small "Powered by
|
|
104
|
+
* Crediball" line by default (set `hideBranding` to remove it).
|
|
105
|
+
*/
|
|
106
|
+
export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, addButtonText, customMin, customMax, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
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/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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
export interface CrediballBalanceRenderProps {
|
|
3
|
+
balance: number | null;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface CrediballBalanceProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
/** Format the raw number. Defaults to a locale-formatted whole number. */
|
|
10
|
+
format?: (balance: number) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Static content overrides the balance entirely. Pass a function to render
|
|
13
|
+
* fully custom markup from the live value (the escape hatch for anything
|
|
14
|
+
* this component's default <span> doesn't cover).
|
|
15
|
+
*/
|
|
16
|
+
children?: ReactNode | ((props: CrediballBalanceRenderProps) => ReactNode);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tier 2 — a "variable" you drop anywhere inside <CrediballProvider>. Renders
|
|
20
|
+
* as an inline <span> that inherits the surrounding font, size, and color
|
|
21
|
+
* (`font: inherit; color: currentColor`) so it looks native wherever it's
|
|
22
|
+
* placed — a navbar, a sentence, a settings row.
|
|
23
|
+
*
|
|
24
|
+
* <p>You have <CrediballBalance /> credits left.</p>
|
|
25
|
+
*/
|
|
26
|
+
export declare function CrediballBalance({ className, style, format, children }: CrediballBalanceProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCrediball } from "../CrediballProvider";
|
|
4
|
+
import { formatCredits } from "../format";
|
|
5
|
+
/**
|
|
6
|
+
* Tier 2 — a "variable" you drop anywhere inside <CrediballProvider>. Renders
|
|
7
|
+
* as an inline <span> that inherits the surrounding font, size, and color
|
|
8
|
+
* (`font: inherit; color: currentColor`) so it looks native wherever it's
|
|
9
|
+
* placed — a navbar, a sentence, a settings row.
|
|
10
|
+
*
|
|
11
|
+
* <p>You have <CrediballBalance /> credits left.</p>
|
|
12
|
+
*/
|
|
13
|
+
export function CrediballBalance({ className, style, format = formatCredits, children }) {
|
|
14
|
+
const { balance, loading } = useCrediball();
|
|
15
|
+
if (typeof children === "function") {
|
|
16
|
+
return _jsx(_Fragment, { children: children({ balance, loading }) });
|
|
17
|
+
}
|
|
18
|
+
return (_jsx("span", { className: className, style: { font: "inherit", color: "currentColor", ...style }, children: children ?? (balance == null ? "—" : format(balance)) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
export interface CrediballLowCreditWarningRenderProps {
|
|
3
|
+
balance: number | null;
|
|
4
|
+
isLow: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface CrediballLowCreditWarningProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
/**
|
|
10
|
+
* Override the provider's default threshold (highest-cost active event) for
|
|
11
|
+
* just this instance — e.g. warn earlier next to an expensive action.
|
|
12
|
+
*/
|
|
13
|
+
threshold?: number;
|
|
14
|
+
/** Message shown when low. Defaults to a generic nudge. */
|
|
15
|
+
message?: string;
|
|
16
|
+
children?: ReactNode | ((props: CrediballLowCreditWarningRenderProps) => ReactNode);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Tier 2 — renders nothing until the user's balance is low, then shows a
|
|
20
|
+
* message inline. Place it next to a specific action ("this costs 20 credits")
|
|
21
|
+
* or globally near the top of the app.
|
|
22
|
+
*
|
|
23
|
+
* <CrediballLowCreditWarning threshold={20} message="Not enough for one more image." />
|
|
24
|
+
*/
|
|
25
|
+
export declare function CrediballLowCreditWarning({ className, style, threshold, message, children, }: CrediballLowCreditWarningProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCrediball } from "../CrediballProvider";
|
|
4
|
+
/**
|
|
5
|
+
* Tier 2 — renders nothing until the user's balance is low, then shows a
|
|
6
|
+
* message inline. Place it next to a specific action ("this costs 20 credits")
|
|
7
|
+
* or globally near the top of the app.
|
|
8
|
+
*
|
|
9
|
+
* <CrediballLowCreditWarning threshold={20} message="Not enough for one more image." />
|
|
10
|
+
*/
|
|
11
|
+
export function CrediballLowCreditWarning({ className, style, threshold, message = "Low credit balance — top up to keep going.", children, }) {
|
|
12
|
+
const { balance, isLow: contextIsLow } = useCrediball();
|
|
13
|
+
const isLow = threshold != null && balance != null ? balance < threshold : contextIsLow;
|
|
14
|
+
if (!isLow)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof children === "function") {
|
|
17
|
+
return _jsx(_Fragment, { children: children({ balance, isLow }) });
|
|
18
|
+
}
|
|
19
|
+
return (_jsx("span", { className: className, style: { font: "inherit", color: "currentColor", ...style }, children: children ?? message }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { type CrediballThemeOverrides } from "../theme";
|
|
3
|
+
export interface CrediballTopUpButtonProps extends CrediballThemeOverrides {
|
|
4
|
+
className?: string;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
/** Defaults to opening the provider's paywall. */
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tier 2 — a themeable call-to-action that opens the paywall. Inherits the
|
|
12
|
+
* surrounding font; background/text color come from the same `--crediball-*`
|
|
13
|
+
* variables as every other component, so it matches PaywallModal and
|
|
14
|
+
* CreditsBadge without extra styling.
|
|
15
|
+
*
|
|
16
|
+
* <CrediballTopUpButton>Get more credits</CrediballTopUpButton>
|
|
17
|
+
*/
|
|
18
|
+
export declare function CrediballTopUpButton({ className, style, children, onClick, accentColor }: CrediballTopUpButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCrediball } from "../CrediballProvider";
|
|
4
|
+
import { theme, accentStyle } from "../theme";
|
|
5
|
+
/**
|
|
6
|
+
* Tier 2 — a themeable call-to-action that opens the paywall. Inherits the
|
|
7
|
+
* surrounding font; background/text color come from the same `--crediball-*`
|
|
8
|
+
* variables as every other component, so it matches PaywallModal and
|
|
9
|
+
* CreditsBadge without extra styling.
|
|
10
|
+
*
|
|
11
|
+
* <CrediballTopUpButton>Get more credits</CrediballTopUpButton>
|
|
12
|
+
*/
|
|
13
|
+
export function CrediballTopUpButton({ className, style, children, onClick, accentColor }) {
|
|
14
|
+
const { openPaywall } = useCrediball();
|
|
15
|
+
return (_jsx("button", { type: "button", className: className, onClick: onClick ?? openPaywall, style: {
|
|
16
|
+
font: "inherit",
|
|
17
|
+
appearance: "none",
|
|
18
|
+
border: "none",
|
|
19
|
+
cursor: "pointer",
|
|
20
|
+
color: theme.onAccent,
|
|
21
|
+
background: theme.accent,
|
|
22
|
+
borderRadius: theme.radiusPill,
|
|
23
|
+
padding: "0.5em 1.1em",
|
|
24
|
+
...accentStyle(accentColor),
|
|
25
|
+
...style,
|
|
26
|
+
}, children: children ?? "Add credits" }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import type { UsagePeriod } from "@crediball/core";
|
|
3
|
+
export interface CrediballUsageRenderProps {
|
|
4
|
+
usage: UsagePeriod | null;
|
|
5
|
+
value: number | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface CrediballUsageProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: CSSProperties;
|
|
11
|
+
/** Show credits consumed this period, or credits remaining. Default "used". */
|
|
12
|
+
show?: "used" | "remaining";
|
|
13
|
+
/** Format the raw number. Defaults to a locale-formatted whole number. */
|
|
14
|
+
format?: (value: number) => string;
|
|
15
|
+
children?: ReactNode | ((props: CrediballUsageRenderProps) => ReactNode);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Tier 2 — credits consumed in the user's current period (their subscription's
|
|
19
|
+
* billing cycle, or the calendar month for non-subscribers). Inline, inherits
|
|
20
|
+
* host styling. Pass `show="remaining"` to render the balance instead.
|
|
21
|
+
*
|
|
22
|
+
* <p><CrediballUsage /> credits used this month.</p>
|
|
23
|
+
*/
|
|
24
|
+
export declare function CrediballUsage({ className, style, show, format, children, }: CrediballUsageProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCrediball } from "../CrediballProvider";
|
|
4
|
+
import { formatCredits } from "../format";
|
|
5
|
+
/**
|
|
6
|
+
* Tier 2 — credits consumed in the user's current period (their subscription's
|
|
7
|
+
* billing cycle, or the calendar month for non-subscribers). Inline, inherits
|
|
8
|
+
* host styling. Pass `show="remaining"` to render the balance instead.
|
|
9
|
+
*
|
|
10
|
+
* <p><CrediballUsage /> credits used this month.</p>
|
|
11
|
+
*/
|
|
12
|
+
export function CrediballUsage({ className, style, show = "used", format = formatCredits, children, }) {
|
|
13
|
+
const { usage, remaining, loading } = useCrediball();
|
|
14
|
+
const value = show === "remaining" ? remaining : (usage?.usedCredits ?? null);
|
|
15
|
+
if (typeof children === "function") {
|
|
16
|
+
return _jsx(_Fragment, { children: children({ usage, value, loading }) });
|
|
17
|
+
}
|
|
18
|
+
return (_jsx("span", { className: className, style: { font: "inherit", color: "currentColor", ...style }, children: children ?? (value == null ? "—" : format(value)) }));
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediball/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Drop-in React components for showing Crediball credits inside your AI app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Filippo Rezzadore <filipporezzadore@gmail.com>",
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": ">=18"
|
|
52
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@crediball/core": "^0.1.1"
|
|
55
|
+
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@types/react": "^18.3.11",
|
|
55
58
|
"typescript": "^5.6.3"
|