@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/LICENSE +22 -0
- package/README.md +48 -0
- package/dist/index.d.mts +434 -0
- package/dist/index.d.ts +434 -0
- package/dist/index.js +935 -0
- package/dist/index.mjs +886 -0
- package/package.json +60 -0
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
|
+
});
|