@meetreeve/billing-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,935 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var src_exports = {};
33
+ __export(src_exports, {
34
+ AutoCheckout: () => AutoCheckout,
35
+ BillingSettingsPanel: () => BillingSettingsPanel,
36
+ BuyMorePacks: () => BuyMorePacks,
37
+ CreditsMeter: () => CreditsMeter,
38
+ LowBalanceBanner: () => LowBalanceBanner,
39
+ PlanCard: () => PlanCard2,
40
+ PricingTable: () => PricingTable,
41
+ UpgradePaywall: () => UpgradePaywall,
42
+ dollarsFromCents: () => dollarsFromCents,
43
+ formatCredits: () => formatCredits,
44
+ formatPrice: () => formatPrice,
45
+ isFeatureUnlocked: () => isFeatureUnlocked,
46
+ isUnlimited: () => isUnlimited,
47
+ resolveUnlockingPlan: () => resolveUnlockingPlan
48
+ });
49
+ module.exports = __toCommonJS(src_exports);
50
+
51
+ // src/format.ts
52
+ function dollarsFromCents(cents) {
53
+ return Math.round(cents / 100);
54
+ }
55
+ function formatPrice(cents, currency = "usd") {
56
+ const symbol = currency.toLowerCase() === "usd" ? "$" : `${currency.toUpperCase()} `;
57
+ return `${symbol}${dollarsFromCents(cents)}`;
58
+ }
59
+ function formatCredits(n) {
60
+ return n.toLocaleString();
61
+ }
62
+
63
+ // src/entitlements.ts
64
+ function resolveUnlockingPlan(plans, featureKey) {
65
+ for (const plan of plans) {
66
+ const limit = plan.feature_limits?.[featureKey];
67
+ if (limit !== void 0 && limit !== 0) return plan;
68
+ }
69
+ return null;
70
+ }
71
+ function isFeatureUnlocked(plan, featureKey) {
72
+ if (!plan) return false;
73
+ const limit = plan.feature_limits?.[featureKey];
74
+ return limit !== void 0 && limit !== 0;
75
+ }
76
+ function isUnlimited(limit) {
77
+ return limit === -1;
78
+ }
79
+
80
+ // src/PricingTable.tsx
81
+ var import_react = require("react");
82
+ var import_jsx_runtime = require("react/jsx-runtime");
83
+ function ctaLabel(plan, currentPlanId, override) {
84
+ if (override) return override;
85
+ if (plan.id === currentPlanId) return "Current plan";
86
+ return `Get ${plan.name}`;
87
+ }
88
+ function PlanCard({
89
+ plan,
90
+ currentPlanId,
91
+ presentation,
92
+ onSelectPlan
93
+ }) {
94
+ const [busy, setBusy] = (0, import_react.useState)(false);
95
+ const [error, setError] = (0, import_react.useState)(null);
96
+ const isCurrent = plan.id === currentPlanId;
97
+ const highlighted = presentation?.highlighted ?? plan.popular ?? false;
98
+ const purchasable = plan.purchasable ?? true;
99
+ const badgeText = isCurrent ? null : presentation?.badge ?? (plan.popular ? "Most popular" : null);
100
+ const creditsLabel = plan.credits_grant_monthly != null ? `${formatCredits(plan.credits_grant_monthly)} credits / month` : plan.credits_grant_lifetime != null ? `${formatCredits(plan.credits_grant_lifetime)} credits, lifetime` : null;
101
+ const handleClick = async () => {
102
+ if (!onSelectPlan || isCurrent || !purchasable) return;
103
+ setBusy(true);
104
+ setError(null);
105
+ try {
106
+ await onSelectPlan(plan);
107
+ } catch (e) {
108
+ setError(e instanceof Error ? e.message : "Could not start checkout.");
109
+ } finally {
110
+ setBusy(false);
111
+ }
112
+ };
113
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
114
+ "div",
115
+ {
116
+ "data-testid": `pricing-plan-${plan.id}`,
117
+ className: "relative flex flex-col rounded-2xl border p-6 " + (highlighted ? "border-primary bg-card shadow-lg" : "border-border bg-card") + (presentation?.className ? ` ${presentation.className}` : ""),
118
+ children: [
119
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
120
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: plan.name }),
121
+ isCurrent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
122
+ "span",
123
+ {
124
+ "data-testid": `pricing-plan-${plan.id}-current-badge`,
125
+ className: "rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary",
126
+ children: "Your plan"
127
+ }
128
+ ) : badgeText ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary", children: badgeText }) : null
129
+ ] }),
130
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-3 flex items-baseline gap-1", children: [
131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-3xl font-bold text-foreground", children: formatPrice(plan.price_cents, plan.currency) }),
132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "text-sm text-muted-foreground", children: [
133
+ "/",
134
+ plan.interval
135
+ ] })
136
+ ] }),
137
+ creditsLabel && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-1 text-sm text-muted-foreground", children: creditsLabel }),
138
+ presentation?.tagline && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-2 text-sm text-muted-foreground", children: presentation.tagline }),
139
+ plan.features.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "mt-4 flex-1 space-y-2 text-sm", children: plan.features.map((f) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: "text-muted-foreground", children: f }, f)) }),
140
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
141
+ "button",
142
+ {
143
+ type: "button",
144
+ onClick: handleClick,
145
+ disabled: isCurrent || busy || !purchasable,
146
+ className: "mt-6 w-full rounded-md bg-foreground px-4 py-2.5 text-sm font-semibold text-background transition-colors disabled:cursor-not-allowed disabled:opacity-60",
147
+ children: busy ? "Starting\u2026" : ctaLabel(plan, currentPlanId, presentation?.cta)
148
+ }
149
+ ),
150
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: "mt-2 text-xs text-destructive", children: error }),
151
+ !purchasable && !isCurrent && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mt-2 text-xs text-muted-foreground", children: "Not available for new subscriptions." })
152
+ ]
153
+ }
154
+ );
155
+ }
156
+ function PackCard({
157
+ pack,
158
+ best,
159
+ onBuyPack
160
+ }) {
161
+ const [busy, setBusy] = (0, import_react.useState)(false);
162
+ const [error, setError] = (0, import_react.useState)(null);
163
+ const dollars = formatPrice(pack.price_cents, pack.currency);
164
+ const perCredit = `$${(dollarsFromCents(pack.price_cents) / pack.credits).toFixed(3)}`;
165
+ const handleClick = async () => {
166
+ if (!onBuyPack) return;
167
+ setBusy(true);
168
+ setError(null);
169
+ try {
170
+ await onBuyPack(pack);
171
+ } catch (e) {
172
+ setError(e instanceof Error ? e.message : "Could not start checkout.");
173
+ } finally {
174
+ setBusy(false);
175
+ }
176
+ };
177
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
178
+ "div",
179
+ {
180
+ "data-testid": `pricing-pack-${pack.id}`,
181
+ className: "flex flex-col gap-4 rounded-2xl border p-5 " + (best ? "border-primary" : "border-border"),
182
+ children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-start justify-between gap-4", children: [
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "text-base font-semibold text-foreground", children: pack.name }),
187
+ best && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] font-semibold uppercase tracking-wide text-primary", children: "Best value" })
188
+ ] }),
189
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "mt-1 text-sm text-muted-foreground", children: [
190
+ formatCredits(pack.credits),
191
+ " credits"
192
+ ] })
193
+ ] }),
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-right", children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xl font-bold text-foreground", children: dollars }),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: [
197
+ perCredit,
198
+ " / credit"
199
+ ] })
200
+ ] })
201
+ ] }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
203
+ "button",
204
+ {
205
+ type: "button",
206
+ onClick: handleClick,
207
+ disabled: busy,
208
+ className: "w-full rounded-md border border-border px-4 py-2 text-sm font-semibold text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
209
+ children: busy ? "Starting\u2026" : `Buy ${pack.name}`
210
+ }
211
+ ),
212
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: "text-xs text-destructive", children: error })
213
+ ]
214
+ }
215
+ );
216
+ }
217
+ function PackGrid({
218
+ packs,
219
+ onBuyPack
220
+ }) {
221
+ if (!packs.length) return null;
222
+ const maxCredits = Math.max(...packs.map((p) => p.credits));
223
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "pricing-packs", className: "mt-10 grid gap-4 md:grid-cols-2 md:gap-6", children: packs.map((pack) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
224
+ PackCard,
225
+ {
226
+ pack,
227
+ best: packs.length > 1 && pack.credits === maxCredits,
228
+ onBuyPack
229
+ },
230
+ pack.id
231
+ )) });
232
+ }
233
+ function PricingTable({
234
+ catalog,
235
+ variant = "grid",
236
+ bandPlanId,
237
+ showPacks,
238
+ currentPlanId,
239
+ presentation,
240
+ onSelectPlan,
241
+ onBuyPack,
242
+ className
243
+ }) {
244
+ const plans = catalog.plans ?? [];
245
+ const packs = catalog.packs ?? [];
246
+ const shouldShowPacks = showPacks ?? packs.length > 0;
247
+ const bandPlan = (0, import_react.useMemo)(() => {
248
+ if (variant !== "band") return null;
249
+ return plans.find((p) => p.id === bandPlanId) ?? plans.find((p) => p.id === currentPlanId) ?? plans.find((p) => p.popular) ?? plans[0] ?? null;
250
+ }, [variant, catalog.plans, bandPlanId, currentPlanId]);
251
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className, "data-testid": "pricing-table", "data-variant": variant, children: [
252
+ variant === "grid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid gap-6 md:grid-cols-3", children: plans.map((plan) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
253
+ PlanCard,
254
+ {
255
+ plan,
256
+ currentPlanId,
257
+ presentation: presentation?.[plan.id],
258
+ onSelectPlan
259
+ },
260
+ plan.id
261
+ )) }) : bandPlan ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mx-auto max-w-md", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
262
+ PlanCard,
263
+ {
264
+ plan: bandPlan,
265
+ currentPlanId,
266
+ presentation: presentation?.[bandPlan.id],
267
+ onSelectPlan
268
+ }
269
+ ) }) : null,
270
+ shouldShowPacks && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PackGrid, { packs, onBuyPack })
271
+ ] });
272
+ }
273
+
274
+ // src/UpgradePaywall.tsx
275
+ var import_react2 = require("react");
276
+ var Dialog = __toESM(require("@radix-ui/react-dialog"));
277
+ var import_jsx_runtime2 = require("react/jsx-runtime");
278
+ function humanizeFeatureKey(key) {
279
+ return key.replace(/_max$/, "").replace(/_/g, " ");
280
+ }
281
+ function PaywallBody({
282
+ featureKey,
283
+ catalog,
284
+ currentPlanId,
285
+ onUpgrade,
286
+ upgradeOptions = ["plan"],
287
+ onAddon,
288
+ addonLabel = "Add capacity",
289
+ secondaryAction,
290
+ title,
291
+ description,
292
+ onClose,
293
+ DescriptionTag = "p"
294
+ }) {
295
+ const [busy, setBusy] = (0, import_react2.useState)(null);
296
+ const [error, setError] = (0, import_react2.useState)(null);
297
+ const currentPlan = (0, import_react2.useMemo)(
298
+ () => catalog.plans.find((p) => p.id === currentPlanId) ?? null,
299
+ [catalog.plans, currentPlanId]
300
+ );
301
+ const alreadyUnlocked = isFeatureUnlocked(currentPlan, featureKey);
302
+ const targetPlan = (0, import_react2.useMemo)(
303
+ () => resolveUnlockingPlan(catalog.plans, featureKey),
304
+ [catalog.plans, featureKey]
305
+ );
306
+ const actionPlan = targetPlan ?? catalog.plans[catalog.plans.length - 1] ?? null;
307
+ const handleUpgrade = async () => {
308
+ if (!onUpgrade || !actionPlan) return;
309
+ setBusy("plan");
310
+ setError(null);
311
+ try {
312
+ await onUpgrade(actionPlan);
313
+ } catch (e) {
314
+ setError(e instanceof Error ? e.message : "Could not start checkout.");
315
+ } finally {
316
+ setBusy(null);
317
+ }
318
+ };
319
+ const handleAddon = async () => {
320
+ if (!onAddon) return;
321
+ setBusy("addon");
322
+ setError(null);
323
+ try {
324
+ await onAddon();
325
+ } catch (e) {
326
+ setError(e instanceof Error ? e.message : "Could not start checkout.");
327
+ } finally {
328
+ setBusy(null);
329
+ }
330
+ };
331
+ const resolvedTitle = title ?? (alreadyUnlocked ? "You already have this" : targetPlan ? `Upgrade to ${targetPlan.name} to unlock this` : "Upgrade to unlock this");
332
+ const resolvedDescription = description ?? (alreadyUnlocked ? `Your current plan already includes ${humanizeFeatureKey(featureKey)}.` : targetPlan ? `${targetPlan.name} unlocks ${humanizeFeatureKey(featureKey)} for ${formatPrice(targetPlan.price_cents, targetPlan.currency)}/${targetPlan.interval}.` : `No plan in your current catalog unlocks ${humanizeFeatureKey(featureKey)} yet \u2014 see all plans to find the right fit.`);
333
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { "data-testid": "upgrade-paywall-body", className: "flex flex-col gap-1", children: [
334
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { className: "text-lg font-semibold text-foreground", children: resolvedTitle }),
335
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DescriptionTag, { className: "text-sm text-muted-foreground", children: resolvedDescription }),
336
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-5 flex flex-col gap-2", children: [
337
+ !alreadyUnlocked && upgradeOptions.includes("plan") && actionPlan && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
338
+ "button",
339
+ {
340
+ type: "button",
341
+ onClick: handleUpgrade,
342
+ disabled: busy !== null,
343
+ className: "inline-flex items-center justify-center gap-2 rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
344
+ children: busy === "plan" ? "Starting\u2026" : `Upgrade to ${actionPlan.name} \xB7 ${formatPrice(actionPlan.price_cents, actionPlan.currency)}/${actionPlan.interval}`
345
+ }
346
+ ),
347
+ !alreadyUnlocked && upgradeOptions.includes("addon") && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
348
+ "button",
349
+ {
350
+ type: "button",
351
+ onClick: handleAddon,
352
+ disabled: busy !== null,
353
+ className: "inline-flex items-center justify-center gap-2 rounded-md border border-border px-5 py-2.5 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
354
+ children: busy === "addon" ? "Starting\u2026" : addonLabel
355
+ }
356
+ ),
357
+ secondaryAction && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
358
+ "button",
359
+ {
360
+ type: "button",
361
+ onClick: secondaryAction.onClick,
362
+ disabled: secondaryAction.disabled,
363
+ className: "inline-flex items-center justify-center gap-2 rounded-md border border-border/70 bg-transparent px-5 py-2.5 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
364
+ children: secondaryAction.label
365
+ }
366
+ ),
367
+ alreadyUnlocked && onClose && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
368
+ "button",
369
+ {
370
+ type: "button",
371
+ onClick: onClose,
372
+ className: "inline-flex items-center justify-center gap-2 rounded-md border border-border px-5 py-2.5 text-sm font-medium text-foreground",
373
+ children: "Close"
374
+ }
375
+ )
376
+ ] }),
377
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "alert", className: "mt-3 text-xs text-destructive", children: error })
378
+ ] });
379
+ }
380
+ function UpgradePaywall(props) {
381
+ const { variant = "modal", open = true, onClose, className, ...bodyProps } = props;
382
+ if (variant === "inline") {
383
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
384
+ "div",
385
+ {
386
+ "data-testid": "upgrade-paywall-inline",
387
+ className: "rounded-2xl border border-border bg-card p-6 " + (className ?? ""),
388
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PaywallBody, { ...bodyProps, onClose })
389
+ }
390
+ );
391
+ }
392
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Root, { open, onOpenChange: (next) => {
393
+ if (!next) onClose?.();
394
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Dialog.Portal, { children: [
395
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" }),
396
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
397
+ Dialog.Content,
398
+ {
399
+ className: "fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-border bg-card p-7 shadow-2xl " + (className ?? ""),
400
+ children: [
401
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Dialog.Title, { className: "sr-only", children: bodyProps.title ?? "Upgrade" }),
402
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
403
+ Dialog.Close,
404
+ {
405
+ "aria-label": "Close",
406
+ className: "absolute right-4 top-4 rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
407
+ children: "\u2715"
408
+ }
409
+ ),
410
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PaywallBody, { ...bodyProps, onClose, DescriptionTag: Dialog.Description })
411
+ ]
412
+ }
413
+ )
414
+ ] }) });
415
+ }
416
+
417
+ // src/AutoCheckout.tsx
418
+ var import_react3 = require("react");
419
+ function AutoCheckout({
420
+ packs = [],
421
+ plans = [],
422
+ onBuyPack,
423
+ onSelectPlan,
424
+ packParam = "credits",
425
+ planParam = "plan"
426
+ }) {
427
+ const firedRef = (0, import_react3.useRef)(false);
428
+ (0, import_react3.useEffect)(() => {
429
+ if (firedRef.current) return;
430
+ if (typeof window === "undefined") return;
431
+ const search = new URLSearchParams(window.location.search);
432
+ const packId = search.get(packParam);
433
+ const planId = search.get(planParam);
434
+ if (packId && onBuyPack && packs.some((p) => p.id === packId)) {
435
+ firedRef.current = true;
436
+ const pack = packs.find((p) => p.id === packId);
437
+ Promise.resolve(onBuyPack(pack)).catch(() => {
438
+ });
439
+ } else if (planId && onSelectPlan && plans.some((p) => p.id === planId)) {
440
+ firedRef.current = true;
441
+ const plan = plans.find((p) => p.id === planId);
442
+ Promise.resolve(onSelectPlan(plan)).catch(() => {
443
+ });
444
+ }
445
+ if (firedRef.current) {
446
+ search.delete(packParam);
447
+ search.delete(planParam);
448
+ const qs = search.toString();
449
+ const nextUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
450
+ window.history.replaceState(window.history.state, "", nextUrl);
451
+ }
452
+ }, [packs, plans, onBuyPack, onSelectPlan]);
453
+ return null;
454
+ }
455
+
456
+ // src/PlanCard.tsx
457
+ var import_react4 = require("react");
458
+ var import_jsx_runtime3 = require("react/jsx-runtime");
459
+ function StatusPill({ status }) {
460
+ const dotColor = status === "active" || status === "trialing" ? "bg-emerald-500" : status === "past_due" ? "bg-amber-500" : status === "canceled" || status === "unpaid" ? "bg-destructive" : "bg-muted-foreground/60";
461
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { "data-testid": "plan-card-status", className: "inline-flex items-center gap-2", children: [
462
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { "aria-hidden": true, className: `h-2 w-2 rounded-full ${dotColor}` }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground", children: status })
464
+ ] });
465
+ }
466
+ function PlanCard2({
467
+ plan,
468
+ renewalDate,
469
+ status,
470
+ isFreePlan = false,
471
+ onManage,
472
+ manageLabel = "Manage billing",
473
+ onChangePlan,
474
+ changePlanLabel,
475
+ className
476
+ }) {
477
+ const [busy, setBusy] = (0, import_react4.useState)(null);
478
+ const [error, setError] = (0, import_react4.useState)(null);
479
+ const runAction = async (kind, action) => {
480
+ setBusy(kind);
481
+ setError(null);
482
+ try {
483
+ await action();
484
+ } catch (e) {
485
+ setError(e instanceof Error ? e.message : "Could not complete that action.");
486
+ } finally {
487
+ setBusy(null);
488
+ }
489
+ };
490
+ if (!plan) {
491
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
492
+ "div",
493
+ {
494
+ "data-testid": "plan-card",
495
+ "data-state": "empty",
496
+ className: "rounded-2xl border border-border bg-card p-6 " + (className ?? ""),
497
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-muted-foreground", children: "No plan on file yet." })
498
+ }
499
+ );
500
+ }
501
+ const showStatusPill = status && status !== "free";
502
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { "data-testid": "plan-card", "data-plan-id": plan.id, className: "rounded-2xl border border-border bg-card p-6 " + (className ?? ""), children: [
503
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [
504
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
505
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-3", children: [
506
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-xl font-semibold text-foreground", children: plan.name }),
507
+ showStatusPill && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusPill, { status })
508
+ ] }),
509
+ renewalDate && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { "data-testid": "plan-card-renewal", className: "mt-1 text-xs uppercase tracking-wide text-muted-foreground", children: [
510
+ "Renews ",
511
+ new Date(renewalDate).toLocaleDateString()
512
+ ] })
513
+ ] }),
514
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "text-right", children: [
515
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: formatPrice(plan.price_cents, plan.currency) }),
516
+ plan.price_cents > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "ml-1 text-xs uppercase tracking-wide text-muted-foreground", children: [
517
+ "/",
518
+ plan.interval
519
+ ] })
520
+ ] })
521
+ ] }),
522
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-5 flex flex-wrap gap-3", children: isFreePlan ? onChangePlan && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
523
+ "button",
524
+ {
525
+ type: "button",
526
+ onClick: () => runAction("change", onChangePlan),
527
+ disabled: busy !== null,
528
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
529
+ children: busy === "change" ? "Loading\u2026" : changePlanLabel ?? "Upgrade"
530
+ }
531
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
532
+ onManage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
533
+ "button",
534
+ {
535
+ type: "button",
536
+ onClick: () => runAction("manage", onManage),
537
+ disabled: busy !== null,
538
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
539
+ children: busy === "manage" ? "Loading\u2026" : manageLabel
540
+ }
541
+ ),
542
+ onChangePlan && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
543
+ "button",
544
+ {
545
+ type: "button",
546
+ onClick: () => runAction("change", onChangePlan),
547
+ disabled: busy !== null,
548
+ className: "rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
549
+ children: busy === "change" ? "Loading\u2026" : changePlanLabel ?? "Change plan"
550
+ }
551
+ )
552
+ ] }) }),
553
+ error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { role: "alert", className: "mt-2 text-xs text-destructive", children: error })
554
+ ] });
555
+ }
556
+
557
+ // src/CreditsMeter.tsx
558
+ var import_jsx_runtime4 = require("react/jsx-runtime");
559
+ function daysUntil(iso) {
560
+ const ms = new Date(iso).getTime() - Date.now();
561
+ return Math.ceil(ms / (1e3 * 60 * 60 * 24));
562
+ }
563
+ function ExpiryNote({ expireAt }) {
564
+ const days = daysUntil(expireAt);
565
+ const label = days < 0 ? "Base credits expired" : days === 0 ? "Base credits expire today" : `Base credits expire in ${days} day${days === 1 ? "" : "s"}`;
566
+ const tone = days <= 0 ? "text-destructive" : days <= 7 ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground";
567
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { "data-testid": "credits-meter-expiry", className: `mt-2 text-xs ${tone}`, children: [
568
+ label,
569
+ " (",
570
+ new Date(expireAt).toLocaleDateString(),
571
+ ")."
572
+ ] });
573
+ }
574
+ function CreditsMeter({ view, balance, className }) {
575
+ if (view.mode === "unavailable") {
576
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { "data-testid": "credits-meter", "data-mode": "unavailable", className, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "text-sm text-muted-foreground", children: [
577
+ "Usage data isn't available yet",
578
+ view.reason ? ` (${view.reason})` : "",
579
+ "."
580
+ ] }) });
581
+ }
582
+ if (view.mode === "percentage") {
583
+ const pct = Math.max(0, Math.min(100, view.percentage_used ?? 0));
584
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { "data-testid": "credits-meter", "data-mode": "percentage", className, children: [
585
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-baseline justify-between gap-3", children: [
586
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-sm font-medium text-foreground", children: "Usage this period" }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { "data-testid": "credits-meter-percentage-value", className: "text-sm text-muted-foreground", children: [
588
+ Math.round(pct),
589
+ "%"
590
+ ] })
591
+ ] }),
592
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "mt-2 h-2 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
593
+ "div",
594
+ {
595
+ className: "h-full rounded-full bg-primary transition-all",
596
+ style: { width: `${pct}%` }
597
+ }
598
+ ) }),
599
+ view.reset_at && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { "data-testid": "credits-meter-reset", className: "mt-2 text-xs text-muted-foreground", children: [
600
+ "Resets ",
601
+ new Date(view.reset_at).toLocaleDateString()
602
+ ] })
603
+ ] });
604
+ }
605
+ const total = balance?.total ?? view.balance ?? 0;
606
+ const expiringSoon = balance?.expiring_soon ?? view.expiring_soon;
607
+ const expireAt = balance?.base_credits_expire_at;
608
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { "data-testid": "credits-meter", "data-mode": "balance", className, children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-baseline gap-2", children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "data-testid": "credits-meter-total", className: "text-3xl font-semibold text-foreground", children: formatCredits(total) }),
611
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "credits available" }),
612
+ balance?.byok_available && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
613
+ "span",
614
+ {
615
+ "data-testid": "credits-meter-byok-badge",
616
+ className: "rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-primary",
617
+ children: "BYOK"
618
+ }
619
+ )
620
+ ] }),
621
+ balance && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("dl", { className: "mt-3 grid grid-cols-2 gap-x-6 gap-y-1 text-sm", children: [
622
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dt", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Plan credits" }),
624
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dd", { "data-testid": "credits-meter-base", className: "text-foreground", children: formatCredits(balance.base_credits) })
625
+ ] }),
626
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
627
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dt", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Top-up credits" }),
628
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("dd", { "data-testid": "credits-meter-topup", className: "text-foreground", children: formatCredits(balance.top_up_credits) })
629
+ ] })
630
+ ] }),
631
+ typeof expiringSoon === "number" && expiringSoon > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { "data-testid": "credits-meter-expiring-soon", className: "mt-2 text-xs text-amber-600 dark:text-amber-400", children: [
632
+ formatCredits(expiringSoon),
633
+ " credits expiring soon."
634
+ ] }),
635
+ expireAt && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ExpiryNote, { expireAt })
636
+ ] });
637
+ }
638
+
639
+ // src/BuyMorePacks.tsx
640
+ var import_react5 = require("react");
641
+ var import_jsx_runtime5 = require("react/jsx-runtime");
642
+ var REPOLL_DELAYS_MS = [0, 2e3, 5e3];
643
+ function PackButton({
644
+ pack,
645
+ compact,
646
+ onPurchase
647
+ }) {
648
+ const [busy, setBusy] = (0, import_react5.useState)(false);
649
+ const [error, setError] = (0, import_react5.useState)(null);
650
+ const handleClick = async () => {
651
+ setBusy(true);
652
+ setError(null);
653
+ try {
654
+ await onPurchase(pack);
655
+ } catch (e) {
656
+ setError(e instanceof Error ? e.message : "Could not start checkout.");
657
+ } finally {
658
+ setBusy(false);
659
+ }
660
+ };
661
+ if (compact) {
662
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "inline-flex flex-col items-start", children: [
663
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
664
+ "button",
665
+ {
666
+ type: "button",
667
+ "data-testid": `buy-more-packs-chip-${pack.id}`,
668
+ onClick: handleClick,
669
+ disabled: busy,
670
+ className: "rounded-full border border-border px-4 py-1.5 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
671
+ children: busy ? "Starting\u2026" : `${formatCredits(pack.credits)} \xB7 ${formatPrice(pack.price_cents, pack.currency)}`
672
+ }
673
+ ),
674
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "alert", className: "mt-1 text-xs text-destructive", children: error })
675
+ ] });
676
+ }
677
+ const perCredit = pack.credits > 0 ? `$${(dollarsFromCents(pack.price_cents) / pack.credits).toFixed(3)}` : null;
678
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
679
+ "div",
680
+ {
681
+ "data-testid": `buy-more-packs-card-${pack.id}`,
682
+ className: "flex flex-col gap-3 rounded-2xl border border-border bg-card p-5",
683
+ children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-start justify-between gap-4", children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h4", { className: "text-base font-semibold text-foreground", children: pack.name }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "mt-1 text-sm text-muted-foreground", children: [
688
+ formatCredits(pack.credits),
689
+ " credits"
690
+ ] })
691
+ ] }),
692
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-right", children: [
693
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-lg font-bold text-foreground", children: formatPrice(pack.price_cents, pack.currency) }),
694
+ perCredit && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: [
695
+ perCredit,
696
+ " / credit"
697
+ ] })
698
+ ] })
699
+ ] }),
700
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
701
+ "button",
702
+ {
703
+ type: "button",
704
+ onClick: handleClick,
705
+ disabled: busy,
706
+ className: "w-full rounded-md border border-border px-4 py-2 text-sm font-semibold text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-60",
707
+ children: busy ? "Starting\u2026" : `Buy ${pack.name}`
708
+ }
709
+ ),
710
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { role: "alert", className: "text-xs text-destructive", children: error })
711
+ ]
712
+ }
713
+ );
714
+ }
715
+ function BuyMorePacks({
716
+ packs,
717
+ purchaseModel = "fixed-pack",
718
+ onPurchase,
719
+ successParam = "topup",
720
+ onTopupSuccess,
721
+ className
722
+ }) {
723
+ const firedRef = (0, import_react5.useRef)(false);
724
+ (0, import_react5.useEffect)(() => {
725
+ if (firedRef.current) return;
726
+ if (typeof window === "undefined") return;
727
+ const search = new URLSearchParams(window.location.search);
728
+ if (search.get(successParam) !== "success") return;
729
+ firedRef.current = true;
730
+ search.delete(successParam);
731
+ const qs = search.toString();
732
+ const nextUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
733
+ window.history.replaceState(window.history.state, "", nextUrl);
734
+ if (!onTopupSuccess) return;
735
+ const timers = REPOLL_DELAYS_MS.map(
736
+ (delay) => setTimeout(() => {
737
+ Promise.resolve(onTopupSuccess()).catch(() => {
738
+ });
739
+ }, delay)
740
+ );
741
+ return () => timers.forEach(clearTimeout);
742
+ }, [successParam, onTopupSuccess]);
743
+ if (!packs.length) return null;
744
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { "data-testid": "buy-more-packs", "data-purchase-model": purchaseModel, className, children: purchaseModel === "presets" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-wrap gap-3", children: packs.map((pack) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PackButton, { pack, compact: true, onPurchase }, pack.id)) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "grid gap-4 md:grid-cols-2", children: packs.map((pack) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PackButton, { pack, compact: false, onPurchase }, pack.id)) }) });
745
+ }
746
+
747
+ // src/LowBalanceBanner.tsx
748
+ var import_react6 = require("react");
749
+ var import_jsx_runtime6 = require("react/jsx-runtime");
750
+ function LowBalanceBanner({
751
+ balance,
752
+ burnRate,
753
+ threshold = 5,
754
+ variant = "default",
755
+ onAddCredits,
756
+ addCreditsLabel = "Add credits",
757
+ onAddByok,
758
+ addByokLabel = "Add a BYOK API key",
759
+ className
760
+ }) {
761
+ const [dismissed, setDismissed] = (0, import_react6.useState)(false);
762
+ (0, import_react6.useEffect)(() => {
763
+ if (balance.total <= 0 && dismissed) setDismissed(false);
764
+ }, [balance.total, dismissed]);
765
+ if (balance.byok_available) return null;
766
+ if (dismissed) return null;
767
+ const isEmpty = balance.total <= 0;
768
+ const isLow = balance.total > 0 && balance.total < threshold;
769
+ const isNearBurnout = !isEmpty && !isLow && burnRate != null && burnRate > 0 && balance.total / burnRate <= 1;
770
+ if (!isEmpty && !isLow && !isNearBurnout) return null;
771
+ const compactCls = variant === "compact" ? "text-xs" : "";
772
+ if (isEmpty) {
773
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
774
+ "div",
775
+ {
776
+ "data-testid": "low-balance-banner",
777
+ "data-state": "empty",
778
+ className: `flex items-center gap-3 rounded-lg border border-primary/20 bg-primary/10 px-4 py-3 ${compactCls} ${className ?? ""}`,
779
+ children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "min-w-0 flex-1", children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: `font-medium text-primary ${variant === "compact" ? "text-xs" : "text-sm"}`, children: "Out of credits." }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: `text-primary/80 ${variant === "compact" ? "text-xs" : "text-sm"}`, children: [
783
+ onAddByok && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
784
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: onAddByok, className: "font-medium underline hover:text-primary", children: addByokLabel }),
785
+ " ",
786
+ "or",
787
+ " "
788
+ ] }),
789
+ onAddCredits && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: onAddCredits, className: "font-medium underline hover:text-primary", children: addCreditsLabel })
790
+ ] })
791
+ ] }),
792
+ onAddCredits && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
793
+ "button",
794
+ {
795
+ type: "button",
796
+ onClick: onAddCredits,
797
+ className: "inline-flex flex-shrink-0 items-center gap-1 rounded-lg bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90",
798
+ children: addCreditsLabel
799
+ }
800
+ )
801
+ ]
802
+ }
803
+ );
804
+ }
805
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
806
+ "div",
807
+ {
808
+ "data-testid": "low-balance-banner",
809
+ "data-state": "low",
810
+ className: `flex items-center gap-3 rounded-lg border border-amber-500/20 bg-amber-500/10 px-4 py-3 ${compactCls} ${className ?? ""}`,
811
+ children: [
812
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: `text-amber-700 dark:text-amber-400 ${variant === "compact" ? "text-xs" : "text-sm"}`, children: [
813
+ "Low credit balance (",
814
+ formatCredits(balance.total),
815
+ " remaining).",
816
+ " ",
817
+ onAddCredits && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", onClick: onAddCredits, className: "font-medium underline hover:text-amber-800 dark:hover:text-amber-300", children: addCreditsLabel })
818
+ ] }) }),
819
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
820
+ "button",
821
+ {
822
+ type: "button",
823
+ onClick: () => setDismissed(true),
824
+ "aria-label": "Dismiss",
825
+ className: "flex-shrink-0 text-amber-500/50 transition-colors hover:text-amber-500",
826
+ children: "\u2715"
827
+ }
828
+ )
829
+ ]
830
+ }
831
+ );
832
+ }
833
+
834
+ // src/BillingSettingsPanel.tsx
835
+ var import_jsx_runtime7 = require("react/jsx-runtime");
836
+ function BillingSettingsPanel({
837
+ plan,
838
+ renewalDate,
839
+ planStatus,
840
+ isFreePlan = false,
841
+ onManagePlan,
842
+ manageLabel,
843
+ onChangePlan,
844
+ changePlanLabel,
845
+ creditsView,
846
+ creditsBalance,
847
+ showLowBalanceBanner = true,
848
+ lowBalanceThreshold,
849
+ burnRate,
850
+ onAddCredits,
851
+ addCreditsLabel,
852
+ onAddByok,
853
+ packs = [],
854
+ purchaseModel = "fixed-pack",
855
+ onBuyPack,
856
+ topupSuccessParam,
857
+ onTopupSuccess,
858
+ catalog,
859
+ showUpgradeSection,
860
+ upgradeVariant,
861
+ upgradePresentation,
862
+ onSelectPlan,
863
+ historySlot,
864
+ className
865
+ }) {
866
+ const renderUpgrade = showUpgradeSection ?? Boolean(catalog);
867
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-testid": "billing-settings-panel", className, children: [
868
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "data-testid": "billing-settings-panel-plan", className: "flex flex-col gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
869
+ PlanCard2,
870
+ {
871
+ plan,
872
+ renewalDate,
873
+ status: planStatus,
874
+ isFreePlan,
875
+ onManage: onManagePlan,
876
+ manageLabel,
877
+ onChangePlan,
878
+ changePlanLabel
879
+ }
880
+ ) }),
881
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "data-testid": "billing-settings-panel-credits", className: "mt-8 flex flex-col gap-3", children: [
882
+ showLowBalanceBanner && creditsBalance && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
883
+ LowBalanceBanner,
884
+ {
885
+ balance: creditsBalance,
886
+ threshold: lowBalanceThreshold,
887
+ burnRate,
888
+ onAddCredits,
889
+ addCreditsLabel,
890
+ onAddByok
891
+ }
892
+ ),
893
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CreditsMeter, { view: creditsView, balance: creditsBalance })
894
+ ] }),
895
+ packs.length > 0 && onBuyPack && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "data-testid": "billing-settings-panel-buy-more", className: "mt-8", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
896
+ BuyMorePacks,
897
+ {
898
+ packs,
899
+ purchaseModel,
900
+ onPurchase: onBuyPack,
901
+ successParam: topupSuccessParam,
902
+ onTopupSuccess
903
+ }
904
+ ) }),
905
+ renderUpgrade && catalog && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "data-testid": "billing-settings-panel-upgrade", className: "mt-8", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
906
+ PricingTable,
907
+ {
908
+ catalog,
909
+ variant: upgradeVariant,
910
+ currentPlanId: plan?.id ?? null,
911
+ presentation: upgradePresentation,
912
+ onSelectPlan,
913
+ showPacks: false
914
+ }
915
+ ) }),
916
+ historySlot && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("section", { "data-testid": "billing-settings-panel-history", className: "mt-8", children: historySlot })
917
+ ] });
918
+ }
919
+ // Annotate the CommonJS export names for ESM import in node:
920
+ 0 && (module.exports = {
921
+ AutoCheckout,
922
+ BillingSettingsPanel,
923
+ BuyMorePacks,
924
+ CreditsMeter,
925
+ LowBalanceBanner,
926
+ PlanCard,
927
+ PricingTable,
928
+ UpgradePaywall,
929
+ dollarsFromCents,
930
+ formatCredits,
931
+ formatPrice,
932
+ isFeatureUnlocked,
933
+ isUnlimited,
934
+ resolveUnlockingPlan
935
+ });