@crediball/react 0.15.0 → 0.17.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.
@@ -65,6 +65,11 @@ function symbolForCurrency(iso) {
65
65
  */
66
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
+ // Surfaced in the paywall itself (not just the console) when the built-in
69
+ // checkout fails — e.g. an invalid/expired promo code — so a user isn't
70
+ // left clicking a "dead" button with no visible feedback. Cleared at the
71
+ // start of every attempt so it always reflects the latest one.
72
+ const [checkoutError, setCheckoutError] = useState(null);
68
73
  // Referral auto-capture: pick up a `?ref=CODE` visit into localStorage (and
69
74
  // count the click) as soon as the provider mounts — the visitor usually has
70
75
  // no account yet at this point.
@@ -104,7 +109,10 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
104
109
  cancelled = true;
105
110
  };
106
111
  }, [publishableKey, userId, apiUrl]);
107
- const showPaywall = useCallback(() => setOpen(true), []);
112
+ const showPaywall = useCallback(() => {
113
+ setCheckoutError(null);
114
+ setOpen(true);
115
+ }, []);
108
116
  const hidePaywall = useCallback(() => setOpen(false), []);
109
117
  // Keep a ref so the fetch-watcher closure always calls the latest version.
110
118
  const showPaywallRef = useRef(showPaywall);
@@ -209,11 +217,13 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
209
217
  return;
210
218
  }
211
219
  if (client) {
220
+ setCheckoutError(null);
212
221
  try {
213
222
  await builtIn();
214
223
  }
215
224
  catch (err) {
216
225
  console.error(`[crediball] ${errorLabel} failed:`, err);
226
+ setCheckoutError(err instanceof Error ? err.message : `Could not start ${errorLabel}. Please try again.`);
217
227
  }
218
228
  return;
219
229
  }
@@ -269,7 +279,7 @@ export function CrediballProvider({ children, publishableKey, userId, apiUrl, po
269
279
  // layout box. Host props/CSS are overridden by design (dashboard wins).
270
280
  const themeVars = applyRemoteTheme ? themeToCssVars(uiTheme) : {};
271
281
  const hasThemeVars = Object.keys(themeVars).length > 0;
272
- 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: effectiveAllowPromoCode, 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 })] }));
282
+ 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: effectiveAllowPromoCode, 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, referredReward: referral?.referredRewardCredits, referralLabel: uiContent?.referralLabel, checkoutError: checkoutError, accentColor: accentColor })] }));
273
283
  const configCtx = useMemo(() => ({ publishableKey, userId, apiUrl }), [publishableKey, userId, apiUrl]);
274
284
  return (_jsx(CrediballContext.Provider, { value: ctx, children: _jsx(CrediballConfigContext.Provider, { value: configCtx, children: hasThemeVars ? (_jsx("div", { style: { display: "contents", ...themeVars }, children: inner })) : (inner) }) }));
275
285
  }
@@ -100,6 +100,13 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
100
100
  * the promo code the user entered, same as `onSelectPackage`'s. */
101
101
  onAdd?: (amount: number, code?: string) => void;
102
102
  onClose?: () => void;
103
+ /**
104
+ * An error from the last checkout attempt (e.g. "Invalid or expired promo
105
+ * code.") to show inline, so a failed purchase isn't silently invisible.
106
+ * Wired automatically by <CrediballProvider> when its built-in checkout
107
+ * fails; pass it yourself when using the modal standalone.
108
+ */
109
+ checkoutError?: string | null;
103
110
  /** When true, show a free-form amount field in addition to the preset buttons. */
104
111
  allowCustom?: boolean;
105
112
  /**
@@ -154,9 +161,18 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
154
161
  /** Credits earned per successful referral, shown in the refer-a-friend blurb. */
155
162
  referralReward?: number;
156
163
  /**
157
- * Label above the referral link. `{credits}` is replaced with `referralReward`.
158
- * Default "Refer a friend · earn {credits} credits". Shown as written (not
159
- * uppercased), so lowercase/sentence case is preserved.
164
+ * Credits the referred friend earns, shown alongside `referralReward` in the
165
+ * refer-a-friend blurb (0/undefined omits that clause). Wired automatically
166
+ * by <CrediballProvider> when referrals are enabled in the dashboard.
167
+ */
168
+ referredReward?: number;
169
+ /**
170
+ * Label above the referral link. `{credits}` is replaced with `referralReward`,
171
+ * `{referredCredits}` with `referredReward`. A `\n` renders as a line break.
172
+ * Defaults to a two-line "Refer a friend.\nYou earn {credits} credits and
173
+ * your friend {referredCredits} credits" when `referredReward` is set, or a
174
+ * single-line "Refer a friend · earn {credits} credits" otherwise. Shown as
175
+ * written (not uppercased), so lowercase/sentence case is preserved.
160
176
  */
161
177
  referralLabel?: string;
162
178
  style?: CSSProperties;
@@ -166,4 +182,4 @@ export interface PaywallModalProps extends CrediballThemeOverrides {
166
182
  * continue. The host controls when it appears; shows a small "Powered by
167
183
  * Crediball" line by default (set `hideBranding` to remove it).
168
184
  */
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;
185
+ export declare function PaywallModal({ open, packages, onSelectPackage, subscriptionPlans, onSelectPlan, activeSubscription, onCancelSubscription, autoTopup, onEnableAutoTopup, amounts, amountPrefix, title, description, onAdd, onClose, checkoutError, allowCustom, allowPromoCode, promoCodeLabel, addButtonText, customMin, customMax, customTopupRate, currencySymbol, balance, balanceRate, currency, usage, usageLabel, hideBranding, referralLink, referralReward, referredReward, referralLabel, accentColor, style, }: PaywallModalProps): import("react").JSX.Element | null;
@@ -9,13 +9,12 @@ const PERIOD_SUFFIX = {
9
9
  monthly: "mo",
10
10
  yearly: "yr",
11
11
  };
12
- /** Shared uppercase eyebrow-label style used above the subscribe/buy-credits sections. */
12
+ /** Shared eyebrow-label style used above the subscribe/buy-credits sections. */
13
13
  function sectionLabelStyle(marginBottom) {
14
14
  return {
15
15
  fontSize: 11,
16
16
  fontWeight: 600,
17
17
  letterSpacing: "0.06em",
18
- textTransform: "uppercase",
19
18
  color: theme.inkMuted,
20
19
  marginBottom,
21
20
  };
@@ -25,7 +24,7 @@ function sectionLabelStyle(marginBottom) {
25
24
  * continue. The host controls when it appears; shows a small "Powered by
26
25
  * Crediball" line by default (set `hideBranding` to remove it).
27
26
  */
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, }) {
27
+ 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, checkoutError, 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, referredReward, referralLabel, accentColor, style, }) {
29
28
  // Stack the top-up buttons one-per-line when the dialog itself is narrow.
30
29
  // Measured off the dialog's own box (via ResizeObserver) rather than the
31
30
  // window — in normal use the two are the same width, but this also makes
@@ -43,6 +42,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
43
42
  const [autoTopupThreshold, setAutoTopupThreshold] = useState("");
44
43
  const [promoCodeOpen, setPromoCodeOpen] = useState(false);
45
44
  const [promoCode, setPromoCode] = useState("");
45
+ const [promoCodeTouched, setPromoCodeTouched] = useState(false);
46
46
  useEffect(() => {
47
47
  const el = overlayRef.current;
48
48
  if (!el || typeof ResizeObserver === "undefined")
@@ -90,6 +90,10 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
90
90
  const trimmed = promoCode.trim();
91
91
  return trimmed || undefined;
92
92
  }
93
+ // Format-only — whether the code is actually active/redeemable is only known
94
+ // server-side when a purchase is made (see enteredCode()'s callers). This
95
+ // just catches an obvious typo/paste mistake before then.
96
+ const promoCodeFormatValid = !promoCode.trim() || /^[A-Z0-9_-]+$/.test(promoCode.trim());
93
97
  function submitCustom() {
94
98
  const amt = Number(customValue);
95
99
  if (!Number.isFinite(amt) || amt <= 0) {
@@ -127,7 +131,37 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
127
131
  padding: "8px 16px",
128
132
  borderRadius: theme.radiusPill,
129
133
  };
130
- const resolvedReferralLabel = referralLabel.replace("{credits}", formatCredits(referralReward ?? 0));
134
+ // Package/plan buttons: name + credits stacked on the left, price on the right.
135
+ const optionButtonStyle = {
136
+ ...buttonStyle,
137
+ flex: "none",
138
+ display: "flex",
139
+ justifyContent: "space-between",
140
+ alignItems: "center",
141
+ gap: 16,
142
+ padding: "10px 16px",
143
+ };
144
+ const optionLeftStyle = {
145
+ display: "flex",
146
+ flexDirection: "column",
147
+ alignItems: "flex-start",
148
+ flex: 1,
149
+ minWidth: 0,
150
+ };
151
+ const optionNameStyle = { fontSize: 17, fontWeight: 600, lineHeight: 1.2 };
152
+ const optionCreditsStyle = { fontSize: 13, opacity: 0.85, lineHeight: 1.2 };
153
+ const optionPriceStyle = {
154
+ fontSize: 17,
155
+ fontWeight: 600,
156
+ whiteSpace: "nowrap",
157
+ flexShrink: 0,
158
+ };
159
+ const defaultReferralLabel = referredReward != null && referredReward > 0
160
+ ? "Refer a friend.\nYou earn {credits} credits and your friend {referredCredits} credits"
161
+ : "Refer a friend · earn {credits} credits";
162
+ const resolvedReferralLabel = (referralLabel ?? defaultReferralLabel)
163
+ .replace("{credits}", formatCredits(referralReward ?? 0))
164
+ .replace("{referredCredits}", formatCredits(referredReward ?? 0));
131
165
  // Same rounding the actual charge uses (see /api/public/topup/checkout),
132
166
  // so this preview never promises a different number than what's granted.
133
167
  const customAmountParsed = Number(customValue);
@@ -154,6 +188,18 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
154
188
  }, onClick: onClose, children: _jsxs("div", { onClick: (e) => e.stopPropagation(), style: {
155
189
  width: "100%",
156
190
  maxWidth: 420,
191
+ // Cap the card to the overlay's own (already viewport-correct, since
192
+ // it's `position: fixed; inset: 0`) content box, and let it scroll
193
+ // internally past that — otherwise a host enabling many optional
194
+ // sections (balance, subscribe, promo code, packages, custom amount,
195
+ // auto top-up, referral) can produce a card taller than the mobile
196
+ // viewport, which the centered overlay then clips top and bottom
197
+ // with no way to reach the rest.
198
+ maxHeight: "100%",
199
+ overflowY: "auto",
200
+ WebkitOverflowScrolling: "touch",
201
+ overscrollBehavior: "contain",
202
+ boxSizing: "border-box",
157
203
  padding: 32,
158
204
  borderRadius: theme.radiusCard,
159
205
  background: theme.canvas,
@@ -171,7 +217,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
171
217
  ? "You've used all your credits for this period."
172
218
  : title }), _jsx("p", { style: { marginTop: 8, marginBottom: 0, fontSize: 13, color: theme.inkMuted }, children: activeSubscription
173
219
  ? `Your ${activeSubscription.planName} credits renew on ${new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()}.`
174
- : description }), activeSubscription && (_jsxs("div", { style: {
220
+ : description }), checkoutError ? (_jsx("p", { style: { marginTop: 12, marginBottom: 0, fontSize: 13, color: theme.accent }, children: checkoutError })) : null, activeSubscription && (_jsxs("div", { style: {
175
221
  marginTop: 16,
176
222
  padding: "12px 16px",
177
223
  borderRadius: theme.radiusCard,
@@ -188,13 +234,22 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
188
234
  color: theme.inkMuted,
189
235
  fontFamily: theme.font,
190
236
  textDecoration: "underline",
191
- }, 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: {
192
- ...buttonStyle,
193
- flex: "none",
194
- display: "flex",
195
- justifyContent: "space-between",
196
- alignItems: "center",
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: {
237
+ }, 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: optionButtonStyle, children: [_jsxs("span", { style: optionLeftStyle, children: [_jsx("span", { style: optionNameStyle, children: plan.name }), _jsxs("span", { style: optionCreditsStyle, children: [formatCredits(plan.credits), " credits"] })] }), _jsxs("span", { style: optionPriceStyle, children: [formatMoney(plan.price, currency), "/", PERIOD_SUFFIX[plan.period]] })] }, 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
238
+ ? packages.map((pkg) => (_jsxs("button", { onClick: () => onSelectPackage?.(pkg, enteredCode()), style: optionButtonStyle, children: [_jsxs("span", { style: optionLeftStyle, children: [_jsx("span", { style: optionNameStyle, children: pkg.name }), _jsxs("span", { style: optionCreditsStyle, children: [formatCredits(pkg.credits), " credits"] })] }), _jsx("span", { style: optionPriceStyle, children: formatMoney(pkg.price, currency) })] }, pkg.id)))
239
+ : (!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: {
240
+ flex: 1,
241
+ minWidth: 0,
242
+ appearance: "none",
243
+ fontFamily: theme.font,
244
+ fontSize: 14,
245
+ lineHeight: "20px",
246
+ padding: "8px 12px",
247
+ borderRadius: theme.radiusPill,
248
+ border: `1px solid ${theme.hairline}`,
249
+ color: theme.ink,
250
+ background: theme.canvas,
251
+ outline: "none",
252
+ } }), _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, 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()), onBlur: () => setPromoCodeTouched(true), placeholder: "PROMOCODE", style: {
198
253
  width: "100%",
199
254
  boxSizing: "border-box",
200
255
  appearance: "none",
@@ -203,11 +258,11 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
203
258
  lineHeight: "20px",
204
259
  padding: "8px 12px",
205
260
  borderRadius: theme.radiusPill,
206
- border: `1px solid ${theme.hairline}`,
261
+ border: `1px solid ${promoCodeTouched && !promoCodeFormatValid ? theme.accent : theme.hairline}`,
207
262
  color: theme.ink,
208
263
  background: theme.canvas,
209
264
  outline: "none",
210
- } })] })) : (_jsx("button", { onClick: () => setPromoCodeOpen(true), style: {
265
+ } }), promoCodeTouched && !promoCodeFormatValid ? (_jsx("p", { style: { margin: "6px 0 0", fontSize: 12, color: theme.accent }, children: "Promo codes only contain letters, numbers, - and _." })) : null] })) : (_jsx("button", { onClick: () => setPromoCodeOpen(true), style: {
211
266
  appearance: "none",
212
267
  border: "none",
213
268
  background: "transparent",
@@ -217,22 +272,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
217
272
  color: theme.inkMuted,
218
273
  fontFamily: theme.font,
219
274
  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: {
223
- flex: 1,
224
- minWidth: 0,
225
- appearance: "none",
226
- fontFamily: theme.font,
227
- fontSize: 14,
228
- lineHeight: "20px",
229
- padding: "8px 12px",
230
- borderRadius: theme.radiusPill,
231
- border: `1px solid ${theme.hairline}`,
232
- color: theme.ink,
233
- background: theme.canvas,
234
- outline: "none",
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"
275
+ }, children: promoCodeLabel })) })), 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"
236
276
  ? "Auto top-up needs you to re-confirm your payment method."
237
277
  : "Auto top-up's last charge failed." }), _jsx("button", { onClick: () => onEnableAutoTopup?.({
238
278
  packageId: autoTopup.packageId,
@@ -298,6 +338,7 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
298
338
  fontWeight: 600,
299
339
  color: theme.inkMuted,
300
340
  marginBottom: 8,
341
+ whiteSpace: "pre-line",
301
342
  }, children: resolvedReferralLabel }), _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [_jsx("div", { title: referralLink, style: {
302
343
  flex: 1,
303
344
  minWidth: 0,
@@ -322,5 +363,5 @@ export function PaywallModal({ open, packages, onSelectPackage, subscriptionPlan
322
363
  padding: "8px 16px",
323
364
  borderRadius: theme.radiusPill,
324
365
  whiteSpace: "nowrap",
325
- }, children: referralCopied ? "Copied!" : "Copy" })] })] })) : 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." }) }))] }) }));
366
+ }, children: referralCopied ? "Copied!" : "Copy" })] })] })) : null, !hideBranding && (_jsx("div", { style: { display: "flex", justifyContent: "flex-end", marginTop: 20 }, children: _jsx("span", { style: { fontSize: 10, color: theme.inkMuted }, children: "Powered by Crediball." }) }))] }) }));
326
367
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediball/react",
3
- "version": "0.15.0",
3
+ "version": "0.17.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>",