@zuplo/zudoku-plugin-monetization 0.0.35 → 0.0.36-pre.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.
@@ -0,0 +1,443 @@
1
+ import { Fragment } from "react";
2
+ import { parse } from "tinyduration";
3
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
4
+ import { clsx } from "clsx";
5
+ import { twMerge } from "tailwind-merge";
6
+ //#region src/utils/formatDuration.ts
7
+ const formatDuration = (iso) => {
8
+ try {
9
+ const d = parse(iso);
10
+ if (d.months === 1) return "month";
11
+ if (d.months && d.months > 1) return `${d.months} months`;
12
+ if (d.years === 1) return "year";
13
+ if (d.years && d.years > 1) return `${d.years} years`;
14
+ if (d.weeks === 1) return "week";
15
+ if (d.weeks && d.weeks > 1) return `${d.weeks} weeks`;
16
+ if (d.days === 1) return "day";
17
+ if (d.days && d.days > 1) return `${d.days} days`;
18
+ return iso;
19
+ } catch {
20
+ return iso;
21
+ }
22
+ };
23
+ const formatDurationInterval = (iso) => {
24
+ try {
25
+ const d = parse(iso);
26
+ if (d.years === 1) return "yearly";
27
+ if (d.years && d.years > 1) return `every ${d.years} years`;
28
+ if (d.months === 1) return "monthly";
29
+ if (d.months && d.months > 1) return `every ${d.months} months`;
30
+ if (d.weeks === 1) return "weekly";
31
+ if (d.weeks && d.weeks > 1) return `every ${d.weeks} weeks`;
32
+ if (d.days === 1) return "daily";
33
+ if (d.days && d.days > 1) return `every ${d.days} days`;
34
+ return iso;
35
+ } catch {
36
+ return iso;
37
+ }
38
+ };
39
+ /**
40
+ * Returns an adjective form suitable for possessive context
41
+ * e.g. "your monthly quota", "your weekly limit".
42
+ * Falls back to "billing period" for multi-unit cadences
43
+ * where "every 3 months" would be grammatically awkward.
44
+ */
45
+ const formatDurationAdjective = (iso) => {
46
+ try {
47
+ const d = parse(iso);
48
+ if (d.years === 1) return "yearly";
49
+ if (d.months === 1) return "monthly";
50
+ if (d.weeks === 1) return "weekly";
51
+ if (d.days === 1) return "daily";
52
+ return "billing period";
53
+ } catch {
54
+ return "billing period";
55
+ }
56
+ };
57
+ //#endregion
58
+ //#region src/utils/formatPrice.ts
59
+ const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
60
+ style: "currency",
61
+ currency: currency ?? "USD",
62
+ minimumFractionDigits: 2,
63
+ maximumFractionDigits: 6,
64
+ trailingZeroDisplay: "stripIfInteger"
65
+ }).format(amount);
66
+ /** Amount is in the smallest currency unit (e.g. Stripe); divisor from `Intl` / ISO 4217. */
67
+ const formatMinorCurrencyAmount = (amountInMinorUnits, currency) => {
68
+ const code = (currency ?? "USD").toUpperCase();
69
+ const fractionDigits = new Intl.NumberFormat("en-US", {
70
+ style: "currency",
71
+ currency: code
72
+ }).resolvedOptions().maximumFractionDigits ?? 2;
73
+ const divisor = 10 ** fractionDigits;
74
+ return new Intl.NumberFormat("en-US", {
75
+ style: "currency",
76
+ currency: code,
77
+ minimumFractionDigits: fractionDigits,
78
+ maximumFractionDigits: fractionDigits
79
+ }).format(amountInMinorUnits / divisor);
80
+ };
81
+ //#endregion
82
+ //#region src/utils/formatStaticEntitlementConfig.ts
83
+ const hasValueField = (value) => typeof value === "object" && value !== null && "value" in value;
84
+ const formatJsonValue = (value) => {
85
+ if (value === void 0) return void 0;
86
+ if (typeof value === "string") return value;
87
+ if (value === null || typeof value === "number" || typeof value === "boolean") return String(value);
88
+ return JSON.stringify(value);
89
+ };
90
+ const formatStaticEntitlementConfig = (config) => {
91
+ if (!config) return void 0;
92
+ try {
93
+ const parsed = JSON.parse(config);
94
+ return formatJsonValue(hasValueField(parsed) ? parsed.value : parsed);
95
+ } catch {
96
+ return;
97
+ }
98
+ };
99
+ //#endregion
100
+ //#region src/utils/formatTieredPriceBreakdown.ts
101
+ const parseAmount = (value) => {
102
+ if (!value) return;
103
+ const parsed = Number.parseFloat(value);
104
+ return Number.isFinite(parsed) ? parsed : void 0;
105
+ };
106
+ const formatTieredPriceBreakdown = (opts) => {
107
+ const { tiers, currency, unitLabel, includedLabel, omitIncludedUpToAmount } = opts;
108
+ if (!tiers || tiers.length <= 1) return;
109
+ const lines = [];
110
+ let lastUpTo;
111
+ for (const tier of tiers) {
112
+ const upTo = parseAmount(tier.upToAmount);
113
+ const unit = parseAmount(tier.unitPriceAmount) ?? 0;
114
+ const flat = parseAmount(tier.flatPriceAmount) ?? 0;
115
+ const prefix = upTo != null ? `Up to ${upTo.toLocaleString("en-US")}` : lastUpTo != null ? `Over ${lastUpTo.toLocaleString("en-US")}` : `Per ${unitLabel}`;
116
+ const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : includedLabel;
117
+ const flatPart = flat > 0 ? ` + ${formatPrice(flat, currency)} base` : "";
118
+ const line = `${prefix}: ${unitPart}${flatPart}`;
119
+ if (omitIncludedUpToAmount != null && upTo != null && upTo === omitIncludedUpToAmount && unitPart === includedLabel && flatPart === "") {} else lines.push(line);
120
+ if (upTo != null) lastUpTo = upTo;
121
+ }
122
+ return lines.length > 0 ? lines : void 0;
123
+ };
124
+ //#endregion
125
+ //#region src/utils/categorizeRateCards.ts
126
+ const categorizeRateCards = (rateCards, options) => {
127
+ const { currency, units, planBillingCadence } = options ?? {};
128
+ const quotas = [];
129
+ const features = [];
130
+ for (const rc of rateCards) {
131
+ const et = rc.entitlementTemplate;
132
+ if (!et) continue;
133
+ if (et.type === "metered" && et.issueAfterReset != null) {
134
+ let overagePrice;
135
+ let tierPrices;
136
+ if (rc.price?.type === "tiered" && rc.price.tiers) {
137
+ const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
138
+ tierPrices = formatTieredPriceBreakdown({
139
+ tiers: rc.price.tiers.map((t) => ({
140
+ upToAmount: t.upToAmount,
141
+ unitPriceAmount: t.unitPrice?.amount,
142
+ flatPriceAmount: t.flatPrice?.amount
143
+ })),
144
+ currency,
145
+ unitLabel,
146
+ includedLabel: "Included",
147
+ omitIncludedUpToAmount: et.issueAfterReset
148
+ });
149
+ const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
150
+ if (et.isSoftLimit !== false && overageTier?.unitPrice) overagePrice = `${formatPrice(parseFloat(overageTier.unitPrice.amount), currency)}/${unitLabel}`;
151
+ }
152
+ quotas.push({
153
+ key: rc.featureKey ?? rc.key,
154
+ name: rc.name,
155
+ limit: et.issueAfterReset,
156
+ period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
157
+ overagePrice,
158
+ tierPrices
159
+ });
160
+ } else if (et.type === "boolean") features.push({
161
+ key: rc.featureKey ?? rc.key,
162
+ name: rc.name
163
+ });
164
+ else if (et.type === "static" && et.config) features.push({
165
+ key: rc.featureKey ?? rc.key,
166
+ name: rc.name,
167
+ value: formatStaticEntitlementConfig(et.config)
168
+ });
169
+ }
170
+ return {
171
+ quotas,
172
+ features
173
+ };
174
+ };
175
+ //#endregion
176
+ //#region src/pricing-ui/CheckIcon.tsx
177
+ /**
178
+ * Inline `Check` icon, visually identical to `lucide-react`'s `CheckIcon`
179
+ * (path: `M20 6 9 17l-5-5`). Kept local so the module has no icon-library
180
+ * peer dep and consumers don't need a specific lucide-react version.
181
+ */
182
+ const CheckIcon = (props) => /* @__PURE__ */ jsx("svg", {
183
+ xmlns: "http://www.w3.org/2000/svg",
184
+ width: 24,
185
+ height: 24,
186
+ viewBox: "0 0 24 24",
187
+ fill: "none",
188
+ stroke: "currentColor",
189
+ strokeWidth: 2,
190
+ strokeLinecap: "round",
191
+ strokeLinejoin: "round",
192
+ "aria-hidden": "true",
193
+ ...props,
194
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
195
+ });
196
+ //#endregion
197
+ //#region src/pricing-ui/cn.ts
198
+ const cn = (...inputs) => twMerge(clsx(inputs));
199
+ //#endregion
200
+ //#region src/pricing-ui/FeatureItem.tsx
201
+ const FeatureItem = ({ feature, className }) => {
202
+ return /* @__PURE__ */ jsxs("div", {
203
+ className: cn("flex items-start gap-2", className),
204
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
205
+ className: "text-sm",
206
+ children: feature.value !== void 0 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
207
+ /* @__PURE__ */ jsxs("span", {
208
+ className: "font-medium",
209
+ children: [feature.name, ":"]
210
+ }),
211
+ " ",
212
+ feature.value
213
+ ] }) : feature.name
214
+ })]
215
+ });
216
+ };
217
+ //#endregion
218
+ //#region src/pricing-ui/QuotaItem.tsx
219
+ const QuotaItem = ({ quota, className }) => {
220
+ return /* @__PURE__ */ jsxs("div", {
221
+ className: cn("flex items-start gap-2", className),
222
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
223
+ className: "text-sm",
224
+ children: [
225
+ /* @__PURE__ */ jsxs("span", {
226
+ className: "font-medium",
227
+ children: [quota.name, ":"]
228
+ }),
229
+ " ",
230
+ quota.limit.toLocaleString(),
231
+ " / ",
232
+ quota.period,
233
+ quota.tierPrices && quota.tierPrices.length > 0 && /* @__PURE__ */ jsx("ul", {
234
+ className: "text-xs text-muted-foreground mt-1 space-y-0.5",
235
+ children: quota.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
236
+ }),
237
+ quota.overagePrice && /* @__PURE__ */ jsxs("div", {
238
+ className: "text-xs text-muted-foreground mt-0.5",
239
+ children: [
240
+ "+",
241
+ quota.overagePrice,
242
+ " after quota"
243
+ ]
244
+ })
245
+ ]
246
+ })]
247
+ });
248
+ };
249
+ //#endregion
250
+ //#region src/pricing-ui/PlanEntitlements.tsx
251
+ const PhaseSection = ({ phase, currency, showName, billingCadence, units, itemClassName }) => {
252
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
253
+ currency,
254
+ units,
255
+ planBillingCadence: billingCadence
256
+ });
257
+ if (quotas.length === 0 && features.length === 0) return null;
258
+ return /* @__PURE__ */ jsxs("div", {
259
+ className: "space-y-2",
260
+ children: [
261
+ showName && /* @__PURE__ */ jsxs("div", {
262
+ className: "text-sm font-medium text-card-foreground",
263
+ children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
264
+ className: "text-muted-foreground font-normal",
265
+ children: [
266
+ " ",
267
+ "— ",
268
+ formatDuration(phase.duration)
269
+ ]
270
+ })]
271
+ }),
272
+ quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
273
+ quota,
274
+ className: itemClassName
275
+ }, quota.key)),
276
+ features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
277
+ feature,
278
+ className: itemClassName
279
+ }, feature.key))
280
+ ]
281
+ });
282
+ };
283
+ const PlanEntitlements = ({ phases, currency, billingCadence, units, itemClassName }) => {
284
+ return /* @__PURE__ */ jsx("div", {
285
+ className: "space-y-4",
286
+ children: phases.map((phase, idx) => /* @__PURE__ */ jsx(PhaseSection, {
287
+ phase,
288
+ currency,
289
+ showName: phases.length > 1,
290
+ billingCadence,
291
+ units,
292
+ itemClassName
293
+ }, phase.key ?? String(idx)))
294
+ });
295
+ };
296
+ //#endregion
297
+ //#region src/utils/getPriceFromPlan.ts
298
+ const getPriceFromPlan = (plan) => {
299
+ return {
300
+ monthly: plan.monthlyPrice != null ? parseFloat(plan.monthlyPrice) : 0,
301
+ yearly: plan.yearlyPrice != null ? parseFloat(plan.yearlyPrice) : 0
302
+ };
303
+ };
304
+ //#endregion
305
+ //#region src/utils/pricingTaxLegend.ts
306
+ const normalizeTaxBehavior = (behavior) => {
307
+ switch (behavior.trim().toLowerCase()) {
308
+ case "exclusive":
309
+ case "tax_exclusive": return "exclusive";
310
+ case "inclusive":
311
+ case "tax_inclusive": return "inclusive";
312
+ default: return "unspecified";
313
+ }
314
+ };
315
+ const planHasDefaultTaxBehavior = (plan) => {
316
+ const behavior = plan.defaultTaxConfig?.behavior;
317
+ return typeof behavior === "string" && behavior.trim().length > 0;
318
+ };
319
+ const collectDefaultTaxBehaviors = (plan) => {
320
+ const behavior = plan.defaultTaxConfig?.behavior;
321
+ return typeof behavior === "string" && behavior.trim().length > 0 ? normalizeTaxBehavior(behavior) : "unspecified";
322
+ };
323
+ const taxBehaviorLegendSentence = (behavior) => {
324
+ switch (normalizeTaxBehavior(behavior)) {
325
+ case "exclusive": return "Prices exclude tax; taxes may be added at checkout if applicable.";
326
+ case "inclusive": return "Prices include tax where applicable.";
327
+ default: return;
328
+ }
329
+ };
330
+ const subscriptionTaxLegendSentence = (behavior) => {
331
+ switch (normalizeTaxBehavior(behavior)) {
332
+ case "exclusive": return "Price excludes tax; taxes may be added on invoice if applicable.";
333
+ case "inclusive": return "Price includes tax where applicable.";
334
+ default: return;
335
+ }
336
+ };
337
+ //#endregion
338
+ //#region src/pricing-ui/PricingCard.tsx
339
+ const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, action, className }) => {
340
+ if (plan.phases.length === 0) return null;
341
+ const price = getPriceFromPlan(plan);
342
+ const isFree = price.monthly === 0;
343
+ const isCustom = plan.metadata?.isCustom === true;
344
+ const billingInterval = formatDuration(plan.billingCadence);
345
+ return /* @__PURE__ */ jsxs("div", {
346
+ className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2", className),
347
+ children: [
348
+ isPopular && /* @__PURE__ */ jsx("div", {
349
+ className: "absolute top-0 -translate-y-1/2 left-1/2 -translate-x-1/2 whitespace-nowrap",
350
+ children: /* @__PURE__ */ jsx("span", {
351
+ className: "bg-primary text-primary-foreground text-xs font-semibold px-3 py-1 rounded-full uppercase",
352
+ children: "Most Popular"
353
+ })
354
+ }),
355
+ /* @__PURE__ */ jsxs("div", {
356
+ className: "mb-4 pb-4 border-b",
357
+ children: [
358
+ /* @__PURE__ */ jsx("h3", {
359
+ className: "text-base font-semibold text-muted-foreground mb-2",
360
+ children: plan.name
361
+ }),
362
+ /* @__PURE__ */ jsx("div", {
363
+ className: "flex items-baseline gap-1 flex-wrap",
364
+ children: isCustom ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
365
+ className: "text-3xl font-bold text-card-foreground",
366
+ children: "Custom"
367
+ }), /* @__PURE__ */ jsx("div", {
368
+ className: "text-sm text-muted-foreground mt-1",
369
+ children: "Contact Sales"
370
+ })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
371
+ className: "text-3xl font-bold text-card-foreground",
372
+ children: isFree ? "Free" : formatPrice(price.monthly, plan.currency)
373
+ }), !isFree && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
374
+ className: "text-muted-foreground text-sm",
375
+ children: ["/", billingInterval]
376
+ }), showYearlyPrice && price.yearly > 0 && /* @__PURE__ */ jsxs("div", {
377
+ className: "w-full text-sm text-muted-foreground mt-1",
378
+ children: [formatPrice(price.yearly, plan.currency), "/year"]
379
+ })] })] })
380
+ }),
381
+ plan.paymentRequired === false && /* @__PURE__ */ jsx("div", {
382
+ className: "text-sm text-muted-foreground mt-1",
383
+ children: "No CC required"
384
+ })
385
+ ]
386
+ }),
387
+ /* @__PURE__ */ jsx("div", {
388
+ className: "space-y-4 mb-6 grow",
389
+ children: /* @__PURE__ */ jsx(PlanEntitlements, {
390
+ phases: plan.phases,
391
+ currency: plan.currency,
392
+ billingCadence: plan.billingCadence,
393
+ units
394
+ })
395
+ }),
396
+ action
397
+ ]
398
+ });
399
+ };
400
+ //#endregion
401
+ //#region src/pricing-ui/PricingTable.tsx
402
+ const DefaultEmptyState = () => /* @__PURE__ */ jsxs("div", {
403
+ className: "text-center py-12 text-muted-foreground",
404
+ children: [/* @__PURE__ */ jsx("p", { children: "No plans are currently available." }), /* @__PURE__ */ jsx("p", {
405
+ className: "text-sm mt-2",
406
+ children: "Make sure your plans are set up and published."
407
+ })]
408
+ });
409
+ const PricingTable = ({ plans, showYearlyPrice = true, units, renderAction, renderCard, isPopular = (plan) => plan.metadata?.zuplo_most_popular === "true", emptyState, showTaxLegend = true, className, cardClassName }) => {
410
+ if (plans.length === 0) return /* @__PURE__ */ jsx(Fragment$1, { children: emptyState ?? /* @__PURE__ */ jsx(DefaultEmptyState, {}) });
411
+ const firstPlan = plans[0];
412
+ const taxLegendSentence = showTaxLegend && firstPlan ? taxBehaviorLegendSentence(collectDefaultTaxBehaviors(firstPlan)) : void 0;
413
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
414
+ className: cn("w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6", className),
415
+ children: plans.map((plan) => {
416
+ const popular = isPopular(plan);
417
+ const defaultCard = /* @__PURE__ */ jsx(PricingCard, {
418
+ plan,
419
+ isPopular: popular,
420
+ showYearlyPrice,
421
+ units,
422
+ action: renderAction?.(plan, popular),
423
+ className: cardClassName
424
+ });
425
+ return /* @__PURE__ */ jsx(Fragment, { children: renderCard ? renderCard(plan, {
426
+ isPopular: popular,
427
+ defaultCard
428
+ }) : defaultCard }, plan.id);
429
+ })
430
+ }), taxLegendSentence && /* @__PURE__ */ jsxs("div", {
431
+ role: "note",
432
+ className: "mt-10 pt-6 border-t border-border max-w-2xl mx-auto text-center space-y-2",
433
+ children: [/* @__PURE__ */ jsx("p", {
434
+ className: "text-xs font-medium text-muted-foreground",
435
+ children: "Tax & Pricing"
436
+ }), /* @__PURE__ */ jsx("p", {
437
+ className: "text-xs text-muted-foreground",
438
+ children: taxLegendSentence
439
+ })]
440
+ })] });
441
+ };
442
+ //#endregion
443
+ export { formatDurationAdjective as _, subscriptionTaxLegendSentence as a, PlanEntitlements as c, categorizeRateCards as d, formatTieredPriceBreakdown as f, formatDuration as g, formatPrice as h, planHasDefaultTaxBehavior as i, QuotaItem as l, formatMinorCurrencyAmount as m, PricingCard as n, taxBehaviorLegendSentence as o, formatStaticEntitlementConfig as p, collectDefaultTaxBehaviors as r, getPriceFromPlan as s, PricingTable as t, FeatureItem as u, formatDurationInterval as v };