@zuplo/zudoku-plugin-monetization 0.0.35 → 0.0.36-pre.1
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/PricingTable-DfYAmAjk.mjs +443 -0
- package/dist/index.mjs +44 -433
- package/dist/pricing-ui.d.mts +296 -0
- package/dist/pricing-ui.mjs +2 -0
- package/package.json +14 -3
|
@@ -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 };
|