@crediball/react 0.11.0 → 0.14.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 +17 -6
- package/dist/CrediballProvider.js +12 -10
- package/dist/PaywallModal.d.ts +37 -14
- package/dist/PaywallModal.js +50 -7
- package/dist/usePaywall.d.ts +6 -4
- package/dist/usePaywall.js +4 -4
- package/package.json +2 -2
|
@@ -32,14 +32,15 @@ export interface CrediballProviderProps {
|
|
|
32
32
|
* userId) and redirects — no backend wiring needed, just connect payouts in the
|
|
33
33
|
* dashboard. Provide this only to run your own top-up/checkout flow instead.
|
|
34
34
|
*/
|
|
35
|
-
onTopup?: (amount: number) => void | Promise<void>;
|
|
35
|
+
onTopup?: (amount: number, code?: string) => void | Promise<void>;
|
|
36
36
|
/**
|
|
37
37
|
* Optional override for one-time packages. If omitted, the paywall starts a
|
|
38
38
|
* Crediball-hosted Stripe Checkout itself and redirects (connect payouts in the
|
|
39
39
|
* dashboard). Provide this only to run your own flow (e.g. `meter.topup({ userId,
|
|
40
|
-
* packageId })` server-side).
|
|
40
|
+
* packageId })` server-side). The second argument is the promo code the user
|
|
41
|
+
* entered, if any (see `allowPromoCode`) — pass it through as `code`.
|
|
41
42
|
*/
|
|
42
|
-
onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
|
|
43
|
+
onSelectPackage?: (pkg: PackageOption, code?: string) => void | Promise<void>;
|
|
43
44
|
/**
|
|
44
45
|
* Optional override for subscription plans. If omitted, the paywall starts a
|
|
45
46
|
* recurring Stripe Checkout itself (connect payouts in the dashboard) and Stripe
|
|
@@ -85,6 +86,15 @@ export interface CrediballProviderProps {
|
|
|
85
86
|
customMin?: number;
|
|
86
87
|
/** Override the custom-amount maximum (defaults to the dashboard's configured maximum). */
|
|
87
88
|
customMax?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Show a "Have a promo code?" field in the paywall (Growth → Promotions'
|
|
91
|
+
* Checkout trigger). Off by default. The code is passed through to
|
|
92
|
+
* `onSelectPackage`/`onTopup`, or included automatically in the built-in
|
|
93
|
+
* checkout when neither is provided.
|
|
94
|
+
*/
|
|
95
|
+
allowPromoCode?: boolean;
|
|
96
|
+
/** Label above the promo code toggle (default "Have a promo code?"). */
|
|
97
|
+
promoCodeLabel?: string;
|
|
88
98
|
/**
|
|
89
99
|
* Currency symbol shown in the paywall. Auto-derived (via Intl) from the ISO
|
|
90
100
|
* currency you configured on the dashboard's Packages page once packages load,
|
|
@@ -153,8 +163,9 @@ interface CrediballContextValue {
|
|
|
153
163
|
error: Error | null;
|
|
154
164
|
/** Credit cost of a tracked event, from the live cost catalog. undefined if unknown. */
|
|
155
165
|
costOf: (event: string) => number | undefined;
|
|
156
|
-
/** Trigger a top-up (delegates to the `onTopup` prop) and refresh balance/usage on completion.
|
|
157
|
-
|
|
166
|
+
/** Trigger a top-up (delegates to the `onTopup` prop) and refresh balance/usage on completion.
|
|
167
|
+
* `code` is a checkout promo code, if any. */
|
|
168
|
+
topUp: (amount: number, code?: string) => Promise<void>;
|
|
158
169
|
/** Refetch balance, usage, costs, and packages immediately. No-op without publishableKey/userId. */
|
|
159
170
|
refresh: () => Promise<void>;
|
|
160
171
|
/** The live packages payload (top-up options, plans, custom-topup, currency, ui). null until loaded. */
|
|
@@ -184,7 +195,7 @@ interface CrediballContextValue {
|
|
|
184
195
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
185
196
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
186
197
|
*/
|
|
187
|
-
export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
|
|
198
|
+
export declare function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts, onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch, allowCustom, customMin, customMax, allowPromoCode, promoCodeLabel, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme, captureReferrals, }: CrediballProviderProps): import("react").JSX.Element;
|
|
188
199
|
/**
|
|
189
200
|
* Access the paywall + live data from anywhere inside <CrediballProvider>.
|
|
190
201
|
*
|
|
@@ -63,7 +63,7 @@ function symbolForCurrency(iso) {
|
|
|
63
63
|
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
64
64
|
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
65
65
|
*/
|
|
66
|
-
export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch = true, allowCustom, customMin, customMax, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme = true, captureReferrals = true, }) {
|
|
66
|
+
export function CrediballProvider({ children, publishableKey, userId, apiUrl, pollIntervalMs, lowCreditThreshold, amounts = [5, 10, 20], onTopup, onSelectPackage, onSelectPlan, onCancelSubscription, onEnableAutoTopup, watchFetch = true, allowCustom, customMin, customMax, allowPromoCode, promoCodeLabel, currencySymbol: currencySymbolProp, title, description, accentColor, applyRemoteTheme = true, captureReferrals = true, }) {
|
|
67
67
|
const [open, setOpen] = useState(false);
|
|
68
68
|
// Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
|
|
69
69
|
// count the click) as soon as the provider mounts — the visitor usually has
|
|
@@ -150,8 +150,8 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
150
150
|
const refresh = useCallback(async () => {
|
|
151
151
|
await client?.refresh();
|
|
152
152
|
}, [client]);
|
|
153
|
-
const topUp = useCallback(async (amount) => {
|
|
154
|
-
await onTopup?.(amount);
|
|
153
|
+
const topUp = useCallback(async (amount, code) => {
|
|
154
|
+
await onTopup?.(amount, code);
|
|
155
155
|
notifyBalanceChanged();
|
|
156
156
|
await client?.refresh();
|
|
157
157
|
}, [onTopup, client]);
|
|
@@ -194,10 +194,12 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
194
194
|
// handler always wins (and refreshes balance/usage on completion); otherwise fall
|
|
195
195
|
// back to the built-in Stripe checkout when a data client is available, leaving the
|
|
196
196
|
// modal open on failure so the error stays visible; otherwise just close the modal.
|
|
197
|
-
|
|
197
|
+
// `code` (a promo code from the paywall) is only meaningful for topup-shaped
|
|
198
|
+
// actions (handleAdd/handleSelectPackage) — other callers simply omit it.
|
|
199
|
+
async function runPaywallAction(hostHandler, arg, builtIn, errorLabel, code) {
|
|
198
200
|
if (hostHandler) {
|
|
199
201
|
try {
|
|
200
|
-
await hostHandler(arg);
|
|
202
|
+
await hostHandler(arg, code);
|
|
201
203
|
notifyBalanceChanged();
|
|
202
204
|
await client?.refresh();
|
|
203
205
|
}
|
|
@@ -218,11 +220,11 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
218
220
|
// Data-less mode with no handler: nothing to charge against.
|
|
219
221
|
setOpen(false);
|
|
220
222
|
}
|
|
221
|
-
function handleAdd(amount) {
|
|
222
|
-
return runPaywallAction(onTopup, amount, () => startBuiltInCheckout({ eurAmount: amount }), "checkout");
|
|
223
|
+
function handleAdd(amount, code) {
|
|
224
|
+
return runPaywallAction(onTopup, amount, () => startBuiltInCheckout({ eurAmount: amount, code }), "checkout", code);
|
|
223
225
|
}
|
|
224
|
-
function handleSelectPackage(pkg) {
|
|
225
|
-
return runPaywallAction(onSelectPackage, pkg, () => startBuiltInCheckout({ packageId: pkg.id }), "checkout");
|
|
226
|
+
function handleSelectPackage(pkg, code) {
|
|
227
|
+
return runPaywallAction(onSelectPackage, pkg, () => startBuiltInCheckout({ packageId: pkg.id, code }), "checkout", code);
|
|
226
228
|
}
|
|
227
229
|
function handleSelectPlan(plan) {
|
|
228
230
|
return runPaywallAction(onSelectPlan, plan, async () => {
|
|
@@ -264,7 +266,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
|
|
|
264
266
|
// layout box. Host props/CSS are overridden by design (dashboard wins).
|
|
265
267
|
const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
|
|
266
268
|
const hasThemeVars = Object.keys(themeVars).length > 0;
|
|
267
|
-
const inner = (_jsxs(_Fragment, { children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: onCancelSubscription ? handleCancelSubscription : undefined, autoTopup: pkgs?.autoTopup, onEnableAutoTopup: handleEnableAutoTopup, 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 ?? uiContent?.paywallTitle, description: description ?? uiContent?.paywallDescription, addButtonText: uiContent?.paywallButtonText, referralLink: referral?.link ?? undefined, referralReward: referral?.rewardCredits, referralLabel: uiContent?.referralLabel, accentColor: accentColor })] }));
|
|
269
|
+
const inner = (_jsxs(_Fragment, { children: [children, _jsx(PaywallModal, { open: open, packages: pkgs?.packages, onSelectPackage: handleSelectPackage, subscriptionPlans: pkgs?.subscriptionPlans, onSelectPlan: handleSelectPlan, activeSubscription: pkgs?.activeSubscription, onCancelSubscription: onCancelSubscription ? handleCancelSubscription : undefined, autoTopup: pkgs?.autoTopup, onEnableAutoTopup: handleEnableAutoTopup, amounts: amounts, amountPrefix: `Add ${currencySymbol}`, onAdd: handleAdd, onClose: hidePaywall, allowCustom: effectiveAllowCustom, customMin: effectiveCustomMin, customMax: effectiveCustomMax, allowPromoCode: allowPromoCode, promoCodeLabel: promoCodeLabel, customTopupRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, currencySymbol: currencySymbol, balance: snapshot.balance ?? undefined, balanceRate: pkgs?.customTopup.enabled ? pkgs.customTopup.rate : undefined, currency: pkgs?.currency, usage: snapshot.usage?.usedCredits ?? undefined, title: title ?? uiContent?.paywallTitle, description: description ?? uiContent?.paywallDescription, addButtonText: uiContent?.paywallButtonText, referralLink: referral?.link ?? undefined, referralReward: referral?.rewardCredits, referralLabel: uiContent?.referralLabel, accentColor: accentColor })] }));
|
|
268
270
|
const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
|
|
269
271
|
return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
|
|
270
272
|
}
|
package/dist/PaywallModal.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface SubscriptionPlanOption {
|
|
|
13
13
|
name: string;
|
|
14
14
|
price: number;
|
|
15
15
|
credits: number;
|
|
16
|
-
period: "monthly" | "yearly";
|
|
16
|
+
period: "weekly" | "monthly" | "yearly";
|
|
17
17
|
}
|
|
18
18
|
/** The end user's current active subscription (returned by listPackages() with userId). */
|
|
19
19
|
export interface ActiveSubscriptionInfo {
|
|
@@ -40,9 +40,11 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
40
40
|
/**
|
|
41
41
|
* Called when the user selects a package. Use this with `packages` — call
|
|
42
42
|
* `meter.topup({ userId, packageId: pkg.id })` in your backend. No `amount`
|
|
43
|
-
* calculation needed; Crediball derives it from the package.
|
|
43
|
+
* calculation needed; Crediball derives it from the package. The second
|
|
44
|
+
* argument is the promo code the user entered (if the "Have a promo code?"
|
|
45
|
+
* field is shown and filled in) — pass it through as `code` to `topup()`.
|
|
44
46
|
*/
|
|
45
|
-
onSelectPackage?: (pkg: PackageOption) => void;
|
|
47
|
+
onSelectPackage?: (pkg: PackageOption, code?: string) => void;
|
|
46
48
|
/**
|
|
47
49
|
* Subscription plans from `meter.listPackages()`. When provided (and the user
|
|
48
50
|
* has no active subscription), the modal shows a "Subscribe" section above
|
|
@@ -67,17 +69,19 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
67
69
|
onCancelSubscription?: (subscriptionId: string) => void | Promise<void>;
|
|
68
70
|
/**
|
|
69
71
|
* The end user's current auto-topup rule (from `meter.listPackages({ userId })`
|
|
70
|
-
* or `meter.getAutoTopupStatus(userId)`). When absent/null
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* or `meter.getAutoTopupStatus(userId)`). When absent/null, an opt-in row is
|
|
73
|
+
* shown below the packages so the user can turn on "keep me topped up
|
|
74
|
+
* automatically".
|
|
73
75
|
*/
|
|
74
76
|
autoTopup?: AutoTopupInfo | null;
|
|
75
77
|
/**
|
|
76
|
-
* Called when the user opts into auto top-up
|
|
77
|
-
* non-empty
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* Called when the user opts into auto top-up. Shown whenever `packages` is
|
|
79
|
+
* non-empty — same as `onSelectPackage`, this is optional to invoke (a no-op
|
|
80
|
+
* if you don't wire it, e.g. in a preview with no backend). Call
|
|
81
|
+
* `meter.setupAutoTopupCheckout({ userId, packageId, thresholdCredits,
|
|
82
|
+
* successUrl, cancelUrl })` in your backend — it saves a payment method, no
|
|
83
|
+
* money changes hands in this step — then redirect the browser to the
|
|
84
|
+
* returned `url`.
|
|
81
85
|
*/
|
|
82
86
|
onEnableAutoTopup?: (input: {
|
|
83
87
|
packageId: string;
|
|
@@ -92,17 +96,36 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
92
96
|
amountPrefix?: string;
|
|
93
97
|
title?: string;
|
|
94
98
|
description?: string;
|
|
95
|
-
/** Called when the user clicks a legacy amount button.
|
|
96
|
-
|
|
99
|
+
/** Called when the user clicks a legacy amount button. The second argument is
|
|
100
|
+
* the promo code the user entered, same as `onSelectPackage`'s. */
|
|
101
|
+
onAdd?: (amount: number, code?: string) => void;
|
|
97
102
|
onClose?: () => void;
|
|
98
103
|
/** When true, show a free-form amount field in addition to the preset buttons. */
|
|
99
104
|
allowCustom?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* When true, show a "Have a promo code?" field above the top-up buttons. The
|
|
107
|
+
* code is passed as the second argument to `onSelectPackage`/`onAdd`/the
|
|
108
|
+
* built-in checkout — pass it through as `code` to `topup()` or
|
|
109
|
+
* `createCheckout()`. Not validated client-side; an invalid code is rejected
|
|
110
|
+
* when the top-up is actually processed.
|
|
111
|
+
*/
|
|
112
|
+
allowPromoCode?: boolean;
|
|
113
|
+
/** Label above the promo code toggle (default "Have a promo code?"). */
|
|
114
|
+
promoCodeLabel?: string;
|
|
100
115
|
/** Button label for the free-form amount field (default "Add"). */
|
|
101
116
|
addButtonText?: string;
|
|
102
117
|
/** Minimum custom amount (default 1). */
|
|
103
118
|
customMin?: number;
|
|
104
119
|
/** Optional maximum custom amount. */
|
|
105
120
|
customMax?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Credits granted per 1 currency unit for the custom-amount field (your
|
|
123
|
+
* dashboard's custom top-up rate). When provided, a live "≈ N credits"
|
|
124
|
+
* line appears under the field as the user types, computed the same way
|
|
125
|
+
* the actual charge is (Math.round(amount * rate)) — so it never promises
|
|
126
|
+
* a different number than what gets granted.
|
|
127
|
+
*/
|
|
128
|
+
customTopupRate?: number;
|
|
106
129
|
/** Currency symbol shown beside the custom field (default "€"). */
|
|
107
130
|
currencySymbol?: string;
|
|
108
131
|
/**
|
|
@@ -143,4 +166,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
|
|
|
143
166
|
* continue. The host controls when it appears; shows a small "Powered by
|
|
144
167
|
* Crediball" line by default (set `hideBranding` to remove it).
|
|
145
168
|
*/
|
|
146
|
-
export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, addButtonText, customMin, customMax, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
|
169
|
+
export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, allowPromoCode, promoCodeLabel, addButtonText, customMin, customMax, customTopupRate, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
|
package/dist/PaywallModal.js
CHANGED
|
@@ -3,6 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { theme, accentStyle } from "./theme";
|
|
5
5
|
import { formatCredits, formatMoney } from "./format";
|
|
6
|
+
/** Shorthand suffix for a billing period, used in plan price labels (e.g. "€9.99/mo"). */
|
|
7
|
+
const PERIOD_SUFFIX = {
|
|
8
|
+
weekly: "wk",
|
|
9
|
+
monthly: "mo",
|
|
10
|
+
yearly: "yr",
|
|
11
|
+
};
|
|
6
12
|
/** Shared uppercase eyebrow-label style used above the subscribe/buy-credits sections. */
|
|
7
13
|
function sectionLabelStyle(marginBottom) {
|
|
8
14
|
return {
|
|
@@ -19,7 +25,7 @@ function sectionLabelStyle(marginBottom) {
|
|
|
19
25
|
* continue. The host controls when it appears; shows a small "Powered by
|
|
20
26
|
* Crediball" line by default (set `hideBranding` to remove it).
|
|
21
27
|
*/
|
|
22
|
-
export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, 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, referralLink, referralReward, referralLabel = "Refer a friend · earn {credits} credits", accentColor, style, }) {
|
|
28
|
+
export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts = [5, 10], amountPrefix = "Add €", title = "You need more credits to continue.", description = "Top up to keep using this feature.", onAdd, onClose, allowCustom = false, allowPromoCode = false, promoCodeLabel = "Have a promo code?", addButtonText = "Add", customMin = 1, customMax, customTopupRate, currencySymbol = "€", balance, balanceRate, currency = "EUR", usage, usageLabel = "Usage", hideBranding = false, referralLink, referralReward, referralLabel = "Refer a friend · earn {credits} credits", accentColor, style, }) {
|
|
23
29
|
// Stack the top-up buttons one-per-line when the dialog itself is narrow.
|
|
24
30
|
// Measured off the dialog's own box (via ResizeObserver) rather than the
|
|
25
31
|
// window — in normal use the two are the same width, but this also makes
|
|
@@ -35,6 +41,8 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
35
41
|
const [autoTopupOpen, setAutoTopupOpen] = useState(false);
|
|
36
42
|
const [autoTopupPackageId, setAutoTopupPackageId] = useState("");
|
|
37
43
|
const [autoTopupThreshold, setAutoTopupThreshold] = useState("");
|
|
44
|
+
const [promoCodeOpen, setPromoCodeOpen] = useState(false);
|
|
45
|
+
const [promoCode, setPromoCode] = useState("");
|
|
38
46
|
useEffect(() => {
|
|
39
47
|
const el = overlayRef.current;
|
|
40
48
|
if (!el || typeof ResizeObserver === "undefined")
|
|
@@ -76,6 +84,12 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
76
84
|
setReferralCopied(true);
|
|
77
85
|
setTimeout(() => setReferralCopied(false), 2000);
|
|
78
86
|
}
|
|
87
|
+
// Undefined (not "") when empty, so onAdd/onSelectPackage receive `code`
|
|
88
|
+
// exactly as the SDK expects (an absent field, not an empty string).
|
|
89
|
+
function enteredCode() {
|
|
90
|
+
const trimmed = promoCode.trim();
|
|
91
|
+
return trimmed || undefined;
|
|
92
|
+
}
|
|
79
93
|
function submitCustom() {
|
|
80
94
|
const amt = Number(customValue);
|
|
81
95
|
if (!Number.isFinite(amt) || amt <= 0) {
|
|
@@ -91,7 +105,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
91
105
|
return;
|
|
92
106
|
}
|
|
93
107
|
setCustomError(null);
|
|
94
|
-
onAdd?.(amt);
|
|
108
|
+
onAdd?.(amt, enteredCode());
|
|
95
109
|
}
|
|
96
110
|
function submitAutoTopup() {
|
|
97
111
|
const threshold = Number(autoTopupThreshold);
|
|
@@ -114,6 +128,12 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
114
128
|
borderRadius: theme.radiusPill,
|
|
115
129
|
};
|
|
116
130
|
const resolvedReferralLabel = referralLabel.replace("{credits}", formatCredits(referralReward ?? 0));
|
|
131
|
+
// Same rounding the actual charge uses (see /api/public/topup/checkout),
|
|
132
|
+
// so this preview never promises a different number than what's granted.
|
|
133
|
+
const customAmountParsed = Number(customValue);
|
|
134
|
+
const customCreditsPreview = customTopupRate && customTopupRate > 0 && Number.isFinite(customAmountParsed) && customAmountParsed > 0
|
|
135
|
+
? Math.round(customAmountParsed * customTopupRate)
|
|
136
|
+
: null;
|
|
117
137
|
return (_jsx("div", { ref: overlayRef, role: "dialog", "aria-modal": "true", style: {
|
|
118
138
|
position: "fixed",
|
|
119
139
|
inset: 0,
|
|
@@ -174,9 +194,32 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
174
194
|
display: "flex",
|
|
175
195
|
justifyContent: "space-between",
|
|
176
196
|
alignItems: "center",
|
|
177
|
-
}, children: [_jsx("span", { children: plan.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [formatMoney(plan.price, currency), "/", plan.period
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
}, children: [_jsx("span", { children: plan.name }), _jsxs("span", { style: { opacity: 0.85, fontSize: 14 }, children: [formatMoney(plan.price, currency), "/", PERIOD_SUFFIX[plan.period], " \u00B7 ", formatCredits(plan.credits), " cr"] })] }, plan.id))) })] })), allowPromoCode && !activeSubscription && (_jsx("div", { style: { marginTop: 16 }, children: promoCodeOpen ? (_jsxs("div", { children: [_jsx("div", { style: { ...sectionLabelStyle(6) }, children: promoCodeLabel }), _jsx("input", { value: promoCode, onChange: (e) => setPromoCode(e.target.value.toUpperCase()), placeholder: "PROMOCODE", style: {
|
|
198
|
+
width: "100%",
|
|
199
|
+
boxSizing: "border-box",
|
|
200
|
+
appearance: "none",
|
|
201
|
+
fontFamily: theme.font,
|
|
202
|
+
fontSize: 14,
|
|
203
|
+
lineHeight: "20px",
|
|
204
|
+
padding: "8px 12px",
|
|
205
|
+
borderRadius: theme.radiusPill,
|
|
206
|
+
border: `1px solid ${theme.hairline}`,
|
|
207
|
+
color: theme.ink,
|
|
208
|
+
background: theme.canvas,
|
|
209
|
+
outline: "none",
|
|
210
|
+
} })] })) : (_jsx("button", { onClick: () => setPromoCodeOpen(true), style: {
|
|
211
|
+
appearance: "none",
|
|
212
|
+
border: "none",
|
|
213
|
+
background: "transparent",
|
|
214
|
+
cursor: "pointer",
|
|
215
|
+
padding: 0,
|
|
216
|
+
fontSize: 12,
|
|
217
|
+
color: theme.inkMuted,
|
|
218
|
+
fontFamily: theme.font,
|
|
219
|
+
textDecoration: "underline",
|
|
220
|
+
}, children: promoCodeLabel })) })), _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
|
|
221
|
+
? packages.map((pkg) => (_jsxs("button", { onClick: () => onSelectPackage?.(pkg, enteredCode()), 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)))
|
|
222
|
+
: (!activeSubscription && (_jsx("div", { style: { display: "flex", flexDirection: narrow ? "column" : "row", gap: 12 }, children: amounts.map((amount) => (_jsxs("button", { onClick: () => onAdd?.(amount, enteredCode()), 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: {
|
|
180
223
|
flex: 1,
|
|
181
224
|
minWidth: 0,
|
|
182
225
|
appearance: "none",
|
|
@@ -189,9 +232,9 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
|
|
|
189
232
|
color: theme.ink,
|
|
190
233
|
background: theme.canvas,
|
|
191
234
|
outline: "none",
|
|
192
|
-
} }), _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,
|
|
235
|
+
} }), _jsx("button", { onClick: submitCustom, style: { ...buttonStyle, flex: "none" }, children: addButtonText })] }), customCreditsPreview != null ? (_jsxs("p", { style: { margin: "6px 0 0", fontSize: 12, color: theme.inkMuted }, children: ["\u2248 ", formatCredits(customCreditsPreview), " credits"] })) : null, customError ? (_jsx("p", { style: { margin: "8px 0 0", fontSize: 13, color: theme.accent }, children: customError })) : null] })) : null, packages && packages.length > 0 && (_jsx("div", { style: { marginTop: 16 }, children: autoTopup?.enabled && autoTopup.status === "active" ? (_jsxs("p", { style: { margin: 0, fontSize: 12, color: theme.inkMuted }, children: ["Auto top-up is on \u2014 buying", " ", packages.find((p) => p.id === autoTopup.packageId)?.name ?? "your package", " whenever your balance drops below ", formatCredits(autoTopup.thresholdCredits), " credits."] })) : autoTopup?.enabled && (autoTopup.status === "needs_auth" || autoTopup.status === "failed") ? (_jsxs("div", { children: [_jsx("p", { style: { margin: 0, fontSize: 12, color: theme.accent }, children: autoTopup.status === "needs_auth"
|
|
193
236
|
? "Auto top-up needs you to re-confirm your payment method."
|
|
194
|
-
: "Auto top-up's last charge failed." }), _jsx("button", { onClick: () => onEnableAutoTopup({
|
|
237
|
+
: "Auto top-up's last charge failed." }), _jsx("button", { onClick: () => onEnableAutoTopup?.({
|
|
195
238
|
packageId: autoTopup.packageId,
|
|
196
239
|
thresholdCredits: autoTopup.thresholdCredits,
|
|
197
240
|
}), style: {
|
package/dist/usePaywall.d.ts
CHANGED
|
@@ -7,10 +7,12 @@ import { type PaywallModalProps, type PackageOption, type SubscriptionPlanOption
|
|
|
7
7
|
* accept async handlers. Add watchFetch to auto-open on detected credit errors.
|
|
8
8
|
*/
|
|
9
9
|
export type UsePaywallProps = Omit<PaywallModalProps, "open" | "onClose" | "onAdd" | "style"> & {
|
|
10
|
-
/** Called when the user picks a legacy preset/custom top-up amount.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/** Called when the user picks a legacy preset/custom top-up amount. The
|
|
11
|
+
* second argument is the promo code entered, if any (see `allowPromoCode`). */
|
|
12
|
+
onTopup?: (amount: number, code?: string) => void | Promise<void>;
|
|
13
|
+
/** Called when the user picks a one-time package (async override). The second
|
|
14
|
+
* argument is the promo code entered, if any (see `allowPromoCode`). */
|
|
15
|
+
onSelectPackage?: (pkg: PackageOption, code?: string) => void | Promise<void>;
|
|
14
16
|
/** Called when the user picks a subscription plan (async override). */
|
|
15
17
|
onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
|
|
16
18
|
/**
|
package/dist/usePaywall.js
CHANGED
|
@@ -36,13 +36,13 @@ export function usePaywall({ onTopup, onSelectPackage, onSelectPlan, watchFetch
|
|
|
36
36
|
return;
|
|
37
37
|
return subscribeFetchWatcher(() => triggerRef.current());
|
|
38
38
|
}, [watchFetch]);
|
|
39
|
-
const handleTopup = useCallback(async (amount) => {
|
|
39
|
+
const handleTopup = useCallback(async (amount, code) => {
|
|
40
40
|
setIsOpen(false);
|
|
41
|
-
await onTopup?.(amount);
|
|
41
|
+
await onTopup?.(amount, code);
|
|
42
42
|
}, [onTopup]);
|
|
43
|
-
const handleSelectPackage = useCallback(async (pkg) => {
|
|
43
|
+
const handleSelectPackage = useCallback(async (pkg, code) => {
|
|
44
44
|
setIsOpen(false);
|
|
45
|
-
await onSelectPackage?.(pkg);
|
|
45
|
+
await onSelectPackage?.(pkg, code);
|
|
46
46
|
}, [onSelectPackage]);
|
|
47
47
|
const handleSelectPlan = useCallback(async (plan) => {
|
|
48
48
|
setIsOpen(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediball/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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>",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react": ">=18"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@crediball/core": "^0.
|
|
54
|
+
"@crediball/core": "^0.8.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/react": "^18.3.11",
|