@zuplo/zudoku-plugin-monetization 0.0.38 → 0.0.40
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.
|
@@ -15,6 +15,12 @@ const formatDuration = (iso) => {
|
|
|
15
15
|
if (d.weeks && d.weeks > 1) return `${d.weeks} weeks`;
|
|
16
16
|
if (d.days === 1) return "day";
|
|
17
17
|
if (d.days && d.days > 1) return `${d.days} days`;
|
|
18
|
+
if (d.hours === 1) return "hour";
|
|
19
|
+
if (d.hours && d.hours > 1) return `${d.hours} hours`;
|
|
20
|
+
if (d.minutes === 1) return "minute";
|
|
21
|
+
if (d.minutes && d.minutes > 1) return `${d.minutes} minutes`;
|
|
22
|
+
if (d.seconds === 1) return "second";
|
|
23
|
+
if (d.seconds && d.seconds > 1) return `${d.seconds} seconds`;
|
|
18
24
|
return iso;
|
|
19
25
|
} catch {
|
|
20
26
|
return iso;
|
|
@@ -31,6 +37,12 @@ const formatDurationInterval = (iso) => {
|
|
|
31
37
|
if (d.weeks && d.weeks > 1) return `every ${d.weeks} weeks`;
|
|
32
38
|
if (d.days === 1) return "daily";
|
|
33
39
|
if (d.days && d.days > 1) return `every ${d.days} days`;
|
|
40
|
+
if (d.hours === 1) return "hourly";
|
|
41
|
+
if (d.hours && d.hours > 1) return `every ${d.hours} hours`;
|
|
42
|
+
if (d.minutes === 1) return "every minute";
|
|
43
|
+
if (d.minutes && d.minutes > 1) return `every ${d.minutes} minutes`;
|
|
44
|
+
if (d.seconds === 1) return "every second";
|
|
45
|
+
if (d.seconds && d.seconds > 1) return `every ${d.seconds} seconds`;
|
|
34
46
|
return iso;
|
|
35
47
|
} catch {
|
|
36
48
|
return iso;
|
|
@@ -40,7 +52,8 @@ const formatDurationInterval = (iso) => {
|
|
|
40
52
|
* Returns an adjective form suitable for possessive context
|
|
41
53
|
* e.g. "your monthly quota", "your weekly limit".
|
|
42
54
|
* Falls back to "billing period" for multi-unit cadences
|
|
43
|
-
* where
|
|
55
|
+
* or sub-hour units where the adjective form is grammatically awkward
|
|
56
|
+
* (e.g. "every 3 months", "every 5 minutes").
|
|
44
57
|
*/
|
|
45
58
|
const formatDurationAdjective = (iso) => {
|
|
46
59
|
try {
|
|
@@ -49,6 +62,7 @@ const formatDurationAdjective = (iso) => {
|
|
|
49
62
|
if (d.months === 1) return "monthly";
|
|
50
63
|
if (d.weeks === 1) return "weekly";
|
|
51
64
|
if (d.days === 1) return "daily";
|
|
65
|
+
if (d.hours === 1) return "hourly";
|
|
52
66
|
return "billing period";
|
|
53
67
|
} catch {
|
|
54
68
|
return "billing period";
|
|
@@ -104,7 +118,7 @@ const parseAmount = (value) => {
|
|
|
104
118
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
105
119
|
};
|
|
106
120
|
const formatTieredPriceBreakdown = (opts) => {
|
|
107
|
-
const { tiers, currency, unitLabel, includedLabel
|
|
121
|
+
const { tiers, currency, unitLabel, includedLabel } = opts;
|
|
108
122
|
if (!tiers || tiers.length <= 1) return;
|
|
109
123
|
const lines = [];
|
|
110
124
|
let lastUpTo;
|
|
@@ -113,10 +127,10 @@ const formatTieredPriceBreakdown = (opts) => {
|
|
|
113
127
|
const unit = parseAmount(tier.unitPriceAmount) ?? 0;
|
|
114
128
|
const flat = parseAmount(tier.flatPriceAmount) ?? 0;
|
|
115
129
|
const prefix = upTo != null ? `Up to ${upTo.toLocaleString("en-US")}` : lastUpTo != null ? `Over ${lastUpTo.toLocaleString("en-US")}` : `Per ${unitLabel}`;
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
130
|
+
const flatPart = flat > 0 ? formatPrice(flat, currency) : "";
|
|
131
|
+
const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : "";
|
|
132
|
+
const pricePart = flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart || includedLabel;
|
|
133
|
+
lines.push(`${prefix}: ${pricePart}`);
|
|
120
134
|
if (upTo != null) lastUpTo = upTo;
|
|
121
135
|
}
|
|
122
136
|
return lines.length > 0 ? lines : void 0;
|
|
@@ -130,32 +144,99 @@ const categorizeRateCards = (rateCards, options) => {
|
|
|
130
144
|
for (const rc of rateCards) {
|
|
131
145
|
const et = rc.entitlementTemplate;
|
|
132
146
|
if (!et) continue;
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
const unitLabelFor = (rcArg) => units?.[rcArg.key] ?? units?.[rcArg.featureKey ?? ""] ?? "unit";
|
|
148
|
+
const periodFor = (rcArg) => {
|
|
149
|
+
if (et.type === "metered" && et.usagePeriod) return formatDuration(et.usagePeriod);
|
|
150
|
+
if (rcArg.billingCadence) return formatDuration(rcArg.billingCadence);
|
|
151
|
+
if (planBillingCadence) return formatDuration(planBillingCadence);
|
|
152
|
+
return "month";
|
|
153
|
+
};
|
|
154
|
+
const firstTier = rc.price?.type === "tiered" && rc.price.tiers.length > 0 ? rc.price.tiers[0] : void 0;
|
|
155
|
+
const firstTierIsPriced = !!firstTier && (parseFloat(firstTier.flatPrice?.amount ?? "0") > 0 || parseFloat(firstTier.unitPrice?.amount ?? "0") > 0);
|
|
156
|
+
if (et.type === "metered" && et.issueAfterReset != null && !firstTierIsPriced) {
|
|
135
157
|
let tierPrices;
|
|
136
|
-
if (rc.price?.type === "tiered" && rc.price.tiers) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
if (rc.price?.type === "tiered" && rc.price.tiers) tierPrices = formatTieredPriceBreakdown({
|
|
159
|
+
tiers: rc.price.tiers.map((t) => ({
|
|
160
|
+
upToAmount: t.upToAmount,
|
|
161
|
+
unitPriceAmount: t.unitPrice?.amount,
|
|
162
|
+
flatPriceAmount: t.flatPrice?.amount
|
|
163
|
+
})),
|
|
164
|
+
currency,
|
|
165
|
+
unitLabel: unitLabelFor(rc),
|
|
166
|
+
includedLabel: "Included"
|
|
167
|
+
});
|
|
168
|
+
quotas.push({
|
|
169
|
+
key: rc.featureKey ?? rc.key,
|
|
170
|
+
name: rc.name,
|
|
171
|
+
limit: et.issueAfterReset,
|
|
172
|
+
period: periodFor(rc),
|
|
173
|
+
tierPrices
|
|
174
|
+
});
|
|
175
|
+
} else if (et.type === "metered" && rc.type === "usage_based" && rc.price) {
|
|
176
|
+
const unitLabel = unitLabelFor(rc);
|
|
177
|
+
if (rc.price.type === "tiered" && rc.price.tiers.length > 0) {
|
|
178
|
+
const tiers = rc.price.tiers;
|
|
179
|
+
if (!tiers.some((t) => parseFloat(t.flatPrice?.amount ?? "0") > 0 || parseFloat(t.unitPrice?.amount ?? "0") > 0)) {
|
|
180
|
+
features.push({
|
|
181
|
+
key: rc.featureKey ?? rc.key,
|
|
182
|
+
name: rc.name
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (tiers.length === 1) {
|
|
187
|
+
const unit = parseFloat(tiers[0].unitPrice?.amount ?? "0");
|
|
188
|
+
const flat = parseFloat(tiers[0].flatPrice?.amount ?? "0");
|
|
189
|
+
const flatPart = flat > 0 ? formatPrice(flat, currency) : "";
|
|
190
|
+
const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : "";
|
|
191
|
+
const pricePart = flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart;
|
|
192
|
+
if (pricePart) {
|
|
193
|
+
quotas.push({
|
|
194
|
+
key: rc.featureKey ?? rc.key,
|
|
195
|
+
name: rc.name,
|
|
196
|
+
limit: 0,
|
|
197
|
+
period: periodFor(rc),
|
|
198
|
+
unitPrice: pricePart,
|
|
199
|
+
isPayg: true
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
features.push({
|
|
204
|
+
key: rc.featureKey ?? rc.key,
|
|
205
|
+
name: rc.name
|
|
206
|
+
});
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const tierPrices = formatTieredPriceBreakdown({
|
|
210
|
+
tiers: tiers.map((t) => ({
|
|
140
211
|
upToAmount: t.upToAmount,
|
|
141
212
|
unitPriceAmount: t.unitPrice?.amount,
|
|
142
213
|
flatPriceAmount: t.flatPrice?.amount
|
|
143
214
|
})),
|
|
144
215
|
currency,
|
|
145
216
|
unitLabel,
|
|
146
|
-
includedLabel: "Included"
|
|
147
|
-
omitIncludedUpToAmount: et.issueAfterReset
|
|
217
|
+
includedLabel: "Included"
|
|
148
218
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
219
|
+
quotas.push({
|
|
220
|
+
key: rc.featureKey ?? rc.key,
|
|
221
|
+
name: rc.name,
|
|
222
|
+
limit: 0,
|
|
223
|
+
period: periodFor(rc),
|
|
224
|
+
tierPrices,
|
|
225
|
+
isPayg: true
|
|
226
|
+
});
|
|
227
|
+
} else if (rc.price.type === "unit" && parseFloat(rc.price.amount) > 0) {
|
|
228
|
+
const amount = parseFloat(rc.price.amount);
|
|
229
|
+
quotas.push({
|
|
230
|
+
key: rc.featureKey ?? rc.key,
|
|
231
|
+
name: rc.name,
|
|
232
|
+
limit: 0,
|
|
233
|
+
period: periodFor(rc),
|
|
234
|
+
unitPrice: `${formatPrice(amount, currency)}/${unitLabel}`,
|
|
235
|
+
isPayg: true
|
|
236
|
+
});
|
|
237
|
+
} else features.push({
|
|
153
238
|
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
|
|
239
|
+
name: rc.name
|
|
159
240
|
});
|
|
160
241
|
} else if (et.type === "boolean") features.push({
|
|
161
242
|
key: rc.featureKey ?? rc.key,
|
|
@@ -217,30 +298,33 @@ const FeatureItem = ({ feature, className }) => {
|
|
|
217
298
|
//#endregion
|
|
218
299
|
//#region src/pricing-ui/QuotaItem.tsx
|
|
219
300
|
const QuotaItem = ({ quota, className }) => {
|
|
301
|
+
const hasTierBreakdown = !!quota.tierPrices && quota.tierPrices.length > 0;
|
|
302
|
+
const showQuotaLine = !quota.isPayg && !hasTierBreakdown;
|
|
220
303
|
return /* @__PURE__ */ jsxs("div", {
|
|
221
304
|
className: cn("flex items-start gap-2", className),
|
|
222
305
|
children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
|
|
223
306
|
className: "text-sm",
|
|
224
307
|
children: [
|
|
225
|
-
/* @__PURE__ */ jsxs(
|
|
308
|
+
showQuotaLine ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
309
|
+
/* @__PURE__ */ jsxs("span", {
|
|
310
|
+
className: "font-medium",
|
|
311
|
+
children: [quota.name, ":"]
|
|
312
|
+
}),
|
|
313
|
+
" ",
|
|
314
|
+
quota.limit.toLocaleString(),
|
|
315
|
+
" / ",
|
|
316
|
+
quota.period
|
|
317
|
+
] }) : /* @__PURE__ */ jsx("span", {
|
|
226
318
|
className: "font-medium",
|
|
227
|
-
children:
|
|
319
|
+
children: quota.name
|
|
228
320
|
}),
|
|
229
|
-
|
|
230
|
-
|
|
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))
|
|
321
|
+
quota.unitPrice && /* @__PURE__ */ jsxs("span", {
|
|
322
|
+
className: "text-muted-foreground",
|
|
323
|
+
children: [" — ", quota.unitPrice]
|
|
236
324
|
}),
|
|
237
|
-
|
|
238
|
-
className: "text-xs text-muted-foreground mt-0.5",
|
|
239
|
-
children:
|
|
240
|
-
"+",
|
|
241
|
-
quota.overagePrice,
|
|
242
|
-
" after quota"
|
|
243
|
-
]
|
|
325
|
+
hasTierBreakdown && /* @__PURE__ */ jsx("ul", {
|
|
326
|
+
className: "text-xs text-muted-foreground mt-1 space-y-0.5",
|
|
327
|
+
children: quota.tierPrices?.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
244
328
|
})
|
|
245
329
|
]
|
|
246
330
|
})]
|
|
@@ -410,11 +494,40 @@ const subscriptionTaxLegendSentence = (behavior) => {
|
|
|
410
494
|
}
|
|
411
495
|
};
|
|
412
496
|
//#endregion
|
|
497
|
+
//#region src/utils/formatPlanPrice.ts
|
|
498
|
+
const isPricedUsageRateCard = (rc) => {
|
|
499
|
+
if (rc.type !== "usage_based" || !rc.price) return false;
|
|
500
|
+
const p = rc.price;
|
|
501
|
+
if (p.type === "unit") return parseFloat(p.amount) > 0;
|
|
502
|
+
if (p.type === "tiered") return p.tiers.some((t) => parseFloat(t.flatPrice?.amount ?? "0") > 0 || parseFloat(t.unitPrice?.amount ?? "0") > 0);
|
|
503
|
+
return true;
|
|
504
|
+
};
|
|
505
|
+
const hasPricedUsageRateCard = (plan) => plan.phases.some((phase) => phase.rateCards.some(isPricedUsageRateCard));
|
|
506
|
+
/**
|
|
507
|
+
* Headline pricing for plan cards. Centralizes the "Pay as you go" detection:
|
|
508
|
+
* plans whose flat-fee total is zero but that bill on usage shouldn't render
|
|
509
|
+
* as "Free" - they're charged per-unit.
|
|
510
|
+
*/
|
|
511
|
+
const formatPlanPrice = (plan) => {
|
|
512
|
+
if (plan.phases.length === 0) return { type: "free" };
|
|
513
|
+
const { monthly, yearly } = getPriceFromPlan(plan);
|
|
514
|
+
if (monthly > 0) return {
|
|
515
|
+
type: "priced",
|
|
516
|
+
monthly,
|
|
517
|
+
yearly
|
|
518
|
+
};
|
|
519
|
+
if (hasPricedUsageRateCard(plan)) return {
|
|
520
|
+
type: "payg",
|
|
521
|
+
main: "Pay as you go",
|
|
522
|
+
sub: "Usage-based pricing"
|
|
523
|
+
};
|
|
524
|
+
return { type: "free" };
|
|
525
|
+
};
|
|
526
|
+
//#endregion
|
|
413
527
|
//#region src/pricing-ui/PricingCard.tsx
|
|
414
528
|
const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, action, className }) => {
|
|
415
529
|
if (plan.phases.length === 0) return null;
|
|
416
|
-
const
|
|
417
|
-
const isFree = price.monthly === 0;
|
|
530
|
+
const priceLabel = formatPlanPrice(plan);
|
|
418
531
|
const isCustom = plan.metadata?.isCustom === true;
|
|
419
532
|
const billingInterval = formatDuration(plan.billingCadence);
|
|
420
533
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -442,16 +555,29 @@ const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, a
|
|
|
442
555
|
}), /* @__PURE__ */ jsx("div", {
|
|
443
556
|
className: "text-sm text-muted-foreground mt-1",
|
|
444
557
|
children: "Contact Sales"
|
|
445
|
-
})] }) : /* @__PURE__ */ jsxs(
|
|
558
|
+
})] }) : priceLabel.type === "payg" ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
559
|
+
className: "text-2xl font-bold text-card-foreground text-balance",
|
|
560
|
+
children: priceLabel.main
|
|
561
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
562
|
+
className: "text-sm text-muted-foreground mt-1",
|
|
563
|
+
children: priceLabel.sub
|
|
564
|
+
})] }) : priceLabel.type === "free" ? /* @__PURE__ */ jsx("span", {
|
|
446
565
|
className: "text-3xl font-bold text-card-foreground",
|
|
447
|
-
children:
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
566
|
+
children: "Free"
|
|
567
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
568
|
+
/* @__PURE__ */ jsx("span", {
|
|
569
|
+
className: "text-3xl font-bold text-card-foreground",
|
|
570
|
+
children: formatPrice(priceLabel.monthly, plan.currency)
|
|
571
|
+
}),
|
|
572
|
+
/* @__PURE__ */ jsxs("span", {
|
|
573
|
+
className: "text-muted-foreground text-sm",
|
|
574
|
+
children: ["/", billingInterval]
|
|
575
|
+
}),
|
|
576
|
+
showYearlyPrice && priceLabel.yearly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
577
|
+
className: "w-full text-sm text-muted-foreground mt-1",
|
|
578
|
+
children: [formatPrice(priceLabel.yearly, plan.currency), "/year"]
|
|
579
|
+
})
|
|
580
|
+
] })
|
|
455
581
|
}),
|
|
456
582
|
plan.paymentRequired === false && /* @__PURE__ */ jsx("div", {
|
|
457
583
|
className: "text-sm text-muted-foreground mt-1",
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, p as formatTieredPriceBreakdown, t as PricingTable, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-
|
|
1
|
+
import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, p as formatTieredPriceBreakdown, t as PricingTable, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-DNop2iX9.mjs";
|
|
2
2
|
import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
|
|
3
3
|
import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
|
|
4
4
|
import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
|
|
@@ -1324,6 +1324,7 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
|
|
|
1324
1324
|
return {
|
|
1325
1325
|
plan: targetPlan,
|
|
1326
1326
|
isUpgrade,
|
|
1327
|
+
isNewerVersion: false,
|
|
1327
1328
|
quotaChanges,
|
|
1328
1329
|
featureChanges
|
|
1329
1330
|
};
|
|
@@ -1334,6 +1335,14 @@ const ChangeIndicator = ({ change }) => {
|
|
|
1334
1335
|
return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
|
|
1335
1336
|
};
|
|
1336
1337
|
const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
|
|
1338
|
+
const planVersion = (plan) => plan.version ?? 1;
|
|
1339
|
+
const isNewerPlanVersion = (subscribedPlan, target) => target.key === subscribedPlan.key && planVersion(target) > planVersion(subscribedPlan);
|
|
1340
|
+
/** Baseline for comparisons: catalog entry when present, else subscription plan. */
|
|
1341
|
+
const resolvePlanForComparison = (subscribedPlan, catalogItems) => catalogItems?.find((p) => p.id === subscribedPlan.id) ?? subscribedPlan;
|
|
1342
|
+
const resolveIsUpgrade = ({ target, targetIndex, subscribedPlan, currentIndex }) => {
|
|
1343
|
+
if (target.key === subscribedPlan.key) return planVersion(target) > planVersion(subscribedPlan);
|
|
1344
|
+
return targetIndex > currentIndex;
|
|
1345
|
+
};
|
|
1337
1346
|
const modeLabelMap = {
|
|
1338
1347
|
upgrade: "Upgrade",
|
|
1339
1348
|
downgrade: "Downgrade",
|
|
@@ -1354,10 +1363,17 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
|
|
|
1354
1363
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1355
1364
|
className: "flex items-center justify-between mb-3",
|
|
1356
1365
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1357
|
-
className: "flex items-baseline gap-2",
|
|
1358
|
-
children: [/* @__PURE__ */
|
|
1359
|
-
className: "
|
|
1360
|
-
children:
|
|
1366
|
+
className: "flex items-baseline gap-2 flex-wrap",
|
|
1367
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1368
|
+
className: "flex items-center gap-2",
|
|
1369
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
1370
|
+
className: "font-semibold text-foreground",
|
|
1371
|
+
children: comparison.plan.name
|
|
1372
|
+
}), comparison.isNewerVersion && /* @__PURE__ */ jsx(Badge, {
|
|
1373
|
+
variant: "outline",
|
|
1374
|
+
className: "rounded-full border-primary/30 bg-primary/10 text-primary font-medium",
|
|
1375
|
+
children: "New version"
|
|
1376
|
+
})]
|
|
1361
1377
|
}), isCustom ? /* @__PURE__ */ jsx("span", {
|
|
1362
1378
|
className: "text-primary font-medium",
|
|
1363
1379
|
children: "Custom"
|
|
@@ -1522,35 +1538,35 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1522
1538
|
window.location.href = data.url;
|
|
1523
1539
|
}
|
|
1524
1540
|
});
|
|
1525
|
-
const
|
|
1541
|
+
const subscribedPlan = subscription.plan;
|
|
1526
1542
|
const { upgrades, downgrades, privatePlans } = useMemo(() => {
|
|
1527
|
-
|
|
1543
|
+
const catalogItems = plansData?.items;
|
|
1544
|
+
if (!catalogItems?.length) return {
|
|
1528
1545
|
upgrades: [],
|
|
1529
1546
|
downgrades: [],
|
|
1530
1547
|
privatePlans: []
|
|
1531
1548
|
};
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
privatePlans: []
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1550
|
-
const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
|
|
1551
|
-
const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
|
|
1552
|
-
return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
|
|
1549
|
+
const planForComparison = resolvePlanForComparison(subscribedPlan, catalogItems);
|
|
1550
|
+
const currentIndex = catalogItems.some((p) => p.id === subscribedPlan.id) ? catalogItems.findIndex((p) => p.id === subscribedPlan.id) : -1;
|
|
1551
|
+
const subscribedIsPrivate = isPrivatePlan(subscribedPlan);
|
|
1552
|
+
const allComparisons = catalogItems.flatMap((plan, targetIndex) => {
|
|
1553
|
+
if (plan.id === subscribedPlan.id) return [];
|
|
1554
|
+
return [{
|
|
1555
|
+
...comparePlans(planForComparison, plan, currentIndex, targetIndex, pricing?.units),
|
|
1556
|
+
isUpgrade: resolveIsUpgrade({
|
|
1557
|
+
target: plan,
|
|
1558
|
+
targetIndex,
|
|
1559
|
+
subscribedPlan,
|
|
1560
|
+
currentIndex
|
|
1561
|
+
}),
|
|
1562
|
+
isNewerVersion: isNewerPlanVersion(subscribedPlan, plan)
|
|
1563
|
+
}];
|
|
1553
1564
|
});
|
|
1565
|
+
if (subscribedIsPrivate) return {
|
|
1566
|
+
upgrades: allComparisons.filter((c) => !isPrivatePlan(c.plan)),
|
|
1567
|
+
downgrades: [],
|
|
1568
|
+
privatePlans: allComparisons.filter((c) => isPrivatePlan(c.plan))
|
|
1569
|
+
};
|
|
1554
1570
|
return {
|
|
1555
1571
|
upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1556
1572
|
downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
@@ -1558,7 +1574,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1558
1574
|
};
|
|
1559
1575
|
}, [
|
|
1560
1576
|
plansData?.items,
|
|
1561
|
-
|
|
1577
|
+
subscribedPlan,
|
|
1562
1578
|
pricing?.units
|
|
1563
1579
|
]);
|
|
1564
1580
|
return /* @__PURE__ */ jsxs(Dialog, {
|
|
@@ -1589,18 +1605,11 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1589
1605
|
children: switchPlanMutation.error.message
|
|
1590
1606
|
})
|
|
1591
1607
|
}),
|
|
1592
|
-
|
|
1593
|
-
variant: "outline",
|
|
1594
|
-
children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
|
|
1595
|
-
className: "text-lg font-bold",
|
|
1596
|
-
children: currentPlan.name
|
|
1597
|
-
})] })
|
|
1598
|
-
}),
|
|
1599
|
-
!currentPlan && /* @__PURE__ */ jsx(Item, {
|
|
1608
|
+
/* @__PURE__ */ jsx(Item, {
|
|
1600
1609
|
variant: "outline",
|
|
1601
1610
|
children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
|
|
1602
1611
|
className: "text-lg font-bold",
|
|
1603
|
-
children:
|
|
1612
|
+
children: subscribedPlan.name
|
|
1604
1613
|
})] })
|
|
1605
1614
|
}),
|
|
1606
1615
|
upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1780,21 +1789,6 @@ const formatDate$1 = (dateString) => {
|
|
|
1780
1789
|
};
|
|
1781
1790
|
const formatDateRange = (from, to) => `${formatDate$1(from)} – ${formatDate$1(to)}`;
|
|
1782
1791
|
const formatNumber = (value) => value.toLocaleString("en-US");
|
|
1783
|
-
const getOveragePriceFromItem = (item, currency, units) => {
|
|
1784
|
-
const tiers = item.price?.tiers;
|
|
1785
|
-
if (!tiers || tiers.length === 0) return void 0;
|
|
1786
|
-
const amount = tiers.find((t) => {
|
|
1787
|
-
const amount = t.unitPrice?.amount;
|
|
1788
|
-
if (!amount) return false;
|
|
1789
|
-
const parsed = parseFloat(amount);
|
|
1790
|
-
return Number.isFinite(parsed) && parsed > 0;
|
|
1791
|
-
})?.unitPrice?.amount;
|
|
1792
|
-
if (!amount) return void 0;
|
|
1793
|
-
const parsed = parseFloat(amount);
|
|
1794
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
1795
|
-
const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
|
|
1796
|
-
return `${formatPrice(parsed, currency)}/${unitLabel}`;
|
|
1797
|
-
};
|
|
1798
1792
|
const getTierPricesFromItem = (item, currency, units) => {
|
|
1799
1793
|
if (item.price?.type !== "tiered") return;
|
|
1800
1794
|
const tiers = item.price.tiers;
|
|
@@ -1808,25 +1802,32 @@ const getTierPricesFromItem = (item, currency, units) => {
|
|
|
1808
1802
|
})),
|
|
1809
1803
|
currency,
|
|
1810
1804
|
unitLabel,
|
|
1811
|
-
includedLabel: "Included"
|
|
1812
|
-
omitIncludedUpToAmount: item.included?.entitlement?.issueAfterReset
|
|
1805
|
+
includedLabel: "Included"
|
|
1813
1806
|
});
|
|
1814
1807
|
};
|
|
1808
|
+
const hasPricedFirstTier = (item) => {
|
|
1809
|
+
const firstTier = item.price?.tiers?.[0];
|
|
1810
|
+
if (!firstTier) return false;
|
|
1811
|
+
const flat = parseFloat(firstTier.flatPrice?.amount ?? "0");
|
|
1812
|
+
const unit = parseFloat(firstTier.unitPrice?.amount ?? "0");
|
|
1813
|
+
return flat > 0 || unit > 0;
|
|
1814
|
+
};
|
|
1815
1815
|
const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
|
|
1816
1816
|
const features = [];
|
|
1817
1817
|
for (const item of items) {
|
|
1818
1818
|
const entitlement = item.included?.entitlement;
|
|
1819
1819
|
if (!entitlement) continue;
|
|
1820
1820
|
if (entitlement.type === "metered" && entitlement.issueAfterReset != null) {
|
|
1821
|
-
const cadence = item.billingCadence ?? fallbackBillingCadence;
|
|
1821
|
+
const cadence = entitlement.usagePeriod?.intervalISO ?? item.billingCadence ?? fallbackBillingCadence;
|
|
1822
|
+
const tierPrices = getTierPricesFromItem(item, currency, units);
|
|
1823
|
+
const suppressLimit = hasPricedFirstTier(item) && !!tierPrices && tierPrices.length > 0;
|
|
1822
1824
|
features.push({
|
|
1823
1825
|
entitlementType: "metered",
|
|
1824
1826
|
key: item.featureKey ?? item.key,
|
|
1825
1827
|
name: item.name ?? item.featureKey ?? item.key,
|
|
1826
|
-
limit: entitlement.issueAfterReset,
|
|
1827
|
-
period: cadence ? formatDuration(cadence) : "month",
|
|
1828
|
-
|
|
1829
|
-
tierPrices: getTierPricesFromItem(item, currency, units)
|
|
1828
|
+
limit: suppressLimit ? void 0 : entitlement.issueAfterReset,
|
|
1829
|
+
period: suppressLimit ? void 0 : cadence ? formatDuration(cadence) : "month",
|
|
1830
|
+
tierPrices
|
|
1830
1831
|
});
|
|
1831
1832
|
continue;
|
|
1832
1833
|
}
|
|
@@ -1872,7 +1873,6 @@ const getPhaseRows = (opts) => {
|
|
|
1872
1873
|
entitlementType: f.entitlementType,
|
|
1873
1874
|
limit: f.entitlementType === "metered" ? f.limit : void 0,
|
|
1874
1875
|
period: f.entitlementType === "metered" ? f.period : void 0,
|
|
1875
|
-
overagePrice: f.entitlementType === "metered" ? f.overagePrice : void 0,
|
|
1876
1876
|
tierPrices: f.entitlementType === "metered" ? f.tierPrices : void 0,
|
|
1877
1877
|
value: f.entitlementType === "static" ? f.value : void 0,
|
|
1878
1878
|
phaseId: phase.id,
|
|
@@ -1992,18 +1992,10 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
1992
1992
|
children: [row.name, row.entitlementType === "static" && row.value !== void 0 ? `: ${row.value}` : ""]
|
|
1993
1993
|
}), /* @__PURE__ */ jsx("div", {
|
|
1994
1994
|
className: "text-muted-foreground",
|
|
1995
|
-
children: row.entitlementType === "metered" && row.limit != null ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1996
|
-
|
|
1997
|
-
row.
|
|
1998
|
-
|
|
1999
|
-
className: "text-xs mt-1 space-y-0.5",
|
|
2000
|
-
children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
2001
|
-
}) : null,
|
|
2002
|
-
row.overagePrice ? /* @__PURE__ */ jsxs("div", {
|
|
2003
|
-
className: "text-xs mt-0.5",
|
|
2004
|
-
children: ["Overage: ", row.overagePrice]
|
|
2005
|
-
}) : null
|
|
2006
|
-
] }) : row.entitlementType === "static" && row.value !== void 0 ? null : "Included"
|
|
1995
|
+
children: row.entitlementType === "metered" && (row.limit != null || row.tierPrices && row.tierPrices.length > 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [row.limit != null && (!row.tierPrices || row.tierPrices.length === 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [formatNumber(row.limit), row.period ? ` / ${row.period}` : ""] }) : null, row.tierPrices && row.tierPrices.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
1996
|
+
className: "text-xs space-y-0.5",
|
|
1997
|
+
children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
1998
|
+
}) : null] }) : row.entitlementType === "static" && row.value !== void 0 ? null : "Included"
|
|
2007
1999
|
})]
|
|
2008
2000
|
})
|
|
2009
2001
|
}, `${row.key}:${row.phaseId}`))
|
package/dist/pricing-ui.d.mts
CHANGED
|
@@ -84,8 +84,9 @@ interface Quota {
|
|
|
84
84
|
name: string;
|
|
85
85
|
limit: number;
|
|
86
86
|
period: string;
|
|
87
|
-
overagePrice?: string;
|
|
88
87
|
tierPrices?: string[];
|
|
88
|
+
isPayg?: boolean;
|
|
89
|
+
unitPrice?: string;
|
|
89
90
|
}
|
|
90
91
|
interface Feature {
|
|
91
92
|
key: string;
|
|
@@ -253,7 +254,8 @@ declare const formatDurationInterval: (iso: string) => string;
|
|
|
253
254
|
* Returns an adjective form suitable for possessive context
|
|
254
255
|
* e.g. "your monthly quota", "your weekly limit".
|
|
255
256
|
* Falls back to "billing period" for multi-unit cadences
|
|
256
|
-
* where
|
|
257
|
+
* or sub-hour units where the adjective form is grammatically awkward
|
|
258
|
+
* (e.g. "every 3 months", "every 5 minutes").
|
|
257
259
|
*/
|
|
258
260
|
declare const formatDurationAdjective: (iso: string) => string;
|
|
259
261
|
//#endregion
|
|
@@ -276,7 +278,6 @@ declare const formatTieredPriceBreakdown: (opts: {
|
|
|
276
278
|
currency?: string;
|
|
277
279
|
unitLabel: string;
|
|
278
280
|
includedLabel: string;
|
|
279
|
-
omitIncludedUpToAmount?: number;
|
|
280
281
|
}) => string[] | undefined;
|
|
281
282
|
//#endregion
|
|
282
283
|
//#region src/utils/getPriceFromPlan.d.ts
|
package/dist/pricing-ui.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, d as FeatureItem, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, n as PricingCard, o as taxBehaviorLegendSentence, p as formatTieredPriceBreakdown, r as collectDefaultTaxBehaviors, s as derivePriceFromPlan, t as PricingTable, u as QuotaItem, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-
|
|
1
|
+
import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, d as FeatureItem, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, n as PricingCard, o as taxBehaviorLegendSentence, p as formatTieredPriceBreakdown, r as collectDefaultTaxBehaviors, s as derivePriceFromPlan, t as PricingTable, u as QuotaItem, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-DNop2iX9.mjs";
|
|
2
2
|
export { FeatureItem, PlanEntitlements, PricingCard, PricingTable, QuotaItem, categorizeRateCards, collectDefaultTaxBehaviors, derivePriceFromPlan, formatDuration, formatDurationAdjective, formatDurationInterval, formatMinorCurrencyAmount, formatPrice, formatStaticEntitlementConfig, formatTieredPriceBreakdown, getPriceFromPlan, planHasDefaultTaxBehavior, subscriptionTaxLegendSentence, taxBehaviorLegendSentence };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuplo/zudoku-plugin-monetization",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zuplo/zudoku",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"react": "19.2.5",
|
|
38
38
|
"react-dom": "19.2.5",
|
|
39
39
|
"tsdown": "0.22.0",
|
|
40
|
-
"zudoku": "0.
|
|
40
|
+
"zudoku": "0.79.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": ">=19.2.0",
|