@crediball/react 0.13.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.
@@ -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
- topUp: (amount: number) => Promise<void>;
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
- async function runPaywallAction(hostHandler, arg, builtIn, errorLabel) {
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, 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 })] }));
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
  }
@@ -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
@@ -94,11 +96,22 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
94
96
  amountPrefix?: string;
95
97
  title?: string;
96
98
  description?: string;
97
- /** Called when the user clicks a legacy amount button. */
98
- onAdd?: (amount: number) => void;
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;
99
102
  onClose?: () => void;
100
103
  /** When true, show a free-form amount field in addition to the preset buttons. */
101
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;
102
115
  /** Button label for the free-form amount field (default "Add"). */
103
116
  addButtonText?: string;
104
117
  /** Minimum custom amount (default 1). */
@@ -153,4 +166,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
153
166
  * continue. The host controls when it appears; shows a small "Powered by
154
167
  * Crediball" line by default (set `hideBranding` to remove it).
155
168
  */
156
- export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, allowCustom, addButtonText, customMin, customMax, customTopupRate, 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;
@@ -25,7 +25,7 @@ function sectionLabelStyle(marginBottom) {
25
25
  * continue. The host controls when it appears; shows a small "Powered by
26
26
  * Crediball" line by default (set `hideBranding` to remove it).
27
27
  */
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, 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, }) {
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, }) {
29
29
  // Stack the top-up buttons one-per-line when the dialog itself is narrow.
30
30
  // Measured off the dialog's own box (via ResizeObserver) rather than the
31
31
  // window — in normal use the two are the same width, but this also makes
@@ -41,6 +41,8 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
41
41
  const [autoTopupOpen, setAutoTopupOpen] = useState(false);
42
42
  const [autoTopupPackageId, setAutoTopupPackageId] = useState("");
43
43
  const [autoTopupThreshold, setAutoTopupThreshold] = useState("");
44
+ const [promoCodeOpen, setPromoCodeOpen] = useState(false);
45
+ const [promoCode, setPromoCode] = useState("");
44
46
  useEffect(() => {
45
47
  const el = overlayRef.current;
46
48
  if (!el || typeof ResizeObserver === "undefined")
@@ -82,6 +84,12 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
82
84
  setReferralCopied(true);
83
85
  setTimeout(() => setReferralCopied(false), 2000);
84
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
+ }
85
93
  function submitCustom() {
86
94
  const amt = Number(customValue);
87
95
  if (!Number.isFinite(amt) || amt <= 0) {
@@ -97,7 +105,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
97
105
  return;
98
106
  }
99
107
  setCustomError(null);
100
- onAdd?.(amt);
108
+ onAdd?.(amt, enteredCode());
101
109
  }
102
110
  function submitAutoTopup() {
103
111
  const threshold = Number(autoTopupThreshold);
@@ -186,9 +194,32 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
186
194
  display: "flex",
187
195
  justifyContent: "space-between",
188
196
  alignItems: "center",
189
- }, 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))) })] })), _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
190
- ? 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)))
191
- : (!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: {
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: {
192
223
  flex: 1,
193
224
  minWidth: 0,
194
225
  appearance: "none",
@@ -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
- onTopup?: (amount: number) => void | Promise<void>;
12
- /** Called when the user picks a one-time package (async override). */
13
- onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
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
  /**
@@ -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.13.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.7.0"
54
+ "@crediball/core": "^0.8.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/react": "^18.3.11",